From 476adefc5e37b1391db19219a872882ac533eb07 Mon Sep 17 00:00:00 2001
From: allenchazhoor <34726956+allenchazhoor@users.noreply.github.com>
Date: Wed, 18 Dec 2024 10:01:21 -0500
Subject: [PATCH 1/2] Merge pull request #875 from
Shopify/add-scopes-update-webhook
Add Scopes Update Webhook to Remix Template
---
.graphqlrc.ts | 40 +++
CHANGELOG.md | 7 +-
app/db.server.ts | 15 +
app/entry.server.tsx | 59 ++++
app/globals.d.ts | 1 +
app/root.tsx | 30 ++
app/routes.ts | 3 +
app/routes/_index/route.tsx | 58 ++++
app/routes/app._index.tsx | 334 ++++++++++++++++++++++
app/routes/app.additional.tsx | 83 ++++++
app/routes/app.tsx | 41 +++
app/routes/auth.$.tsx | 8 +
app/routes/auth.login/error.server.tsx | 16 ++
app/routes/auth.login/route.tsx | 68 +++++
app/routes/webhooks.app.scopes_update.tsx | 21 ++
app/routes/webhooks.app.uninstalled.tsx | 17 ++
app/shopify.server.ts | 35 +++
shopify.app.toml | 5 +
vite.config.ts | 66 +++++
19 files changed, 905 insertions(+), 2 deletions(-)
create mode 100644 .graphqlrc.ts
create mode 100644 app/db.server.ts
create mode 100644 app/entry.server.tsx
create mode 100644 app/globals.d.ts
create mode 100644 app/root.tsx
create mode 100644 app/routes.ts
create mode 100644 app/routes/_index/route.tsx
create mode 100644 app/routes/app._index.tsx
create mode 100644 app/routes/app.additional.tsx
create mode 100644 app/routes/app.tsx
create mode 100644 app/routes/auth.$.tsx
create mode 100644 app/routes/auth.login/error.server.tsx
create mode 100644 app/routes/auth.login/route.tsx
create mode 100644 app/routes/webhooks.app.scopes_update.tsx
create mode 100644 app/routes/webhooks.app.uninstalled.tsx
create mode 100644 app/shopify.server.ts
create mode 100644 vite.config.ts
diff --git a/.graphqlrc.ts b/.graphqlrc.ts
new file mode 100644
index 00000000..ad9c5f81
--- /dev/null
+++ b/.graphqlrc.ts
@@ -0,0 +1,40 @@
+import fs from "fs";
+import { LATEST_API_VERSION } from "@shopify/shopify-api";
+import { shopifyApiProject, ApiType } from "@shopify/api-codegen-preset";
+import type { IGraphQLConfig } from "graphql-config";
+
+function getConfig() {
+ const config: IGraphQLConfig = {
+ projects: {
+ default: shopifyApiProject({
+ apiType: ApiType.Admin,
+ apiVersion: LATEST_API_VERSION,
+ documents: ["./app/**/*.{js,ts,jsx,tsx}", "./app/.server/**/*.{js,ts,jsx,tsx}"],
+ outputDir: "./app/types",
+ }),
+ },
+ };
+
+ let extensions: string[] = [];
+ try {
+ extensions = fs.readdirSync("./extensions");
+ } catch {
+ // ignore if no extensions
+ }
+
+ for (const entry of extensions) {
+ const extensionPath = `./extensions/${entry}`;
+ const schema = `${extensionPath}/schema.graphql`;
+ if (!fs.existsSync(schema)) {
+ continue;
+ }
+ config.projects[entry] = {
+ schema,
+ documents: [`${extensionPath}/**/*.graphql`],
+ };
+ }
+
+ return config;
+}
+
+module.exports = getConfig();
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d4dc2ab3..427e152b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
# @shopify/shopify-app-template-remix
+## 2024.12.18
+
+- [875](https://github.com/Shopify/shopify-app-template-remix/pull/875) Add Scopes Update Webhook
+
## 2024.12.05
- [#910](https://github.com/Shopify/shopify-app-template-remix/pull/910) Install `openssl` in Docker image to fix Prisma (see [#25817](https://github.com/prisma/prisma/issues/25817#issuecomment-2538544254))
@@ -12,7 +16,6 @@
- [#891](https://github.com/Shopify/shopify-app-template-remix/pull/891) Enable remix future flags.
## 2024.11.26
-
- [888](https://github.com/Shopify/shopify-app-template-remix/pull/888) Update restResources version to 2024-10
## 2024.11.06
@@ -33,7 +36,7 @@
## 2024.09.17
-- [842](https://github.com/Shopify/shopify-app-template-remix/pull/842)Move webhook processing to individual routes
+- [842](https://github.com/Shopify/shopify-app-template-remix/pull/842) Move webhook processing to individual routes
## 2024.08.19
diff --git a/app/db.server.ts b/app/db.server.ts
new file mode 100644
index 00000000..bafa6cc2
--- /dev/null
+++ b/app/db.server.ts
@@ -0,0 +1,15 @@
+import { PrismaClient } from "@prisma/client";
+
+declare global {
+ var prisma: PrismaClient;
+}
+
+if (process.env.NODE_ENV !== "production") {
+ if (!global.prisma) {
+ global.prisma = new PrismaClient();
+ }
+}
+
+const prisma: PrismaClient = global.prisma || new PrismaClient();
+
+export default prisma;
diff --git a/app/entry.server.tsx b/app/entry.server.tsx
new file mode 100644
index 00000000..86274311
--- /dev/null
+++ b/app/entry.server.tsx
@@ -0,0 +1,59 @@
+import { PassThrough } from "stream";
+import { renderToPipeableStream } from "react-dom/server";
+import { RemixServer } from "@remix-run/react";
+import {
+ createReadableStreamFromReadable,
+ type EntryContext,
+} from "@remix-run/node";
+import { isbot } from "isbot";
+import { addDocumentResponseHeaders } from "./shopify.server";
+
+export const streamTimeout = 5000;
+
+export default async function handleRequest(
+ request: Request,
+ responseStatusCode: number,
+ responseHeaders: Headers,
+ remixContext: EntryContext
+) {
+ addDocumentResponseHeaders(request, responseHeaders);
+ const userAgent = request.headers.get("user-agent");
+ const callbackName = isbot(userAgent ?? '')
+ ? "onAllReady"
+ : "onShellReady";
+
+ return new Promise((resolve, reject) => {
+ const { pipe, abort } = renderToPipeableStream(
+ ,
+ {
+ [callbackName]: () => {
+ const body = new PassThrough();
+ const stream = createReadableStreamFromReadable(body);
+
+ responseHeaders.set("Content-Type", "text/html");
+ resolve(
+ new Response(stream, {
+ headers: responseHeaders,
+ status: responseStatusCode,
+ })
+ );
+ pipe(body);
+ },
+ onShellError(error) {
+ reject(error);
+ },
+ onError(error) {
+ responseStatusCode = 500;
+ console.error(error);
+ },
+ }
+ );
+
+ // Automatically timeout the React renderer after 6 seconds, which ensures
+ // React has enough time to flush down the rejected boundary contents
+ setTimeout(abort, streamTimeout + 1000);
+ });
+}
diff --git a/app/globals.d.ts b/app/globals.d.ts
new file mode 100644
index 00000000..cbe652db
--- /dev/null
+++ b/app/globals.d.ts
@@ -0,0 +1 @@
+declare module "*.css";
diff --git a/app/root.tsx b/app/root.tsx
new file mode 100644
index 00000000..805f121f
--- /dev/null
+++ b/app/root.tsx
@@ -0,0 +1,30 @@
+import {
+ Links,
+ Meta,
+ Outlet,
+ Scripts,
+ ScrollRestoration,
+} from "@remix-run/react";
+
+export default function App() {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/app/routes.ts b/app/routes.ts
new file mode 100644
index 00000000..83892841
--- /dev/null
+++ b/app/routes.ts
@@ -0,0 +1,3 @@
+import { flatRoutes } from "@remix-run/fs-routes";
+
+export default flatRoutes();
diff --git a/app/routes/_index/route.tsx b/app/routes/_index/route.tsx
new file mode 100644
index 00000000..2de9dd47
--- /dev/null
+++ b/app/routes/_index/route.tsx
@@ -0,0 +1,58 @@
+import type { LoaderFunctionArgs } from "@remix-run/node";
+import { redirect } from "@remix-run/node";
+import { Form, useLoaderData } from "@remix-run/react";
+
+import { login } from "../../shopify.server";
+
+import styles from "./styles.module.css";
+
+export const loader = async ({ request }: LoaderFunctionArgs) => {
+ const url = new URL(request.url);
+
+ if (url.searchParams.get("shop")) {
+ throw redirect(`/app?${url.searchParams.toString()}`);
+ }
+
+ return { showForm: Boolean(login) };
+};
+
+export default function App() {
+ const { showForm } = useLoaderData();
+
+ return (
+
+
+
A short heading about [your app]
+
+ A tagline about [your app] that describes your value proposition.
+
+ {showForm && (
+
+ )}
+
+ -
+ Product feature. Some detail about your feature and
+ its benefit to your customer.
+
+ -
+ Product feature. Some detail about your feature and
+ its benefit to your customer.
+
+ -
+ Product feature. Some detail about your feature and
+ its benefit to your customer.
+
+
+
+
+ );
+}
diff --git a/app/routes/app._index.tsx b/app/routes/app._index.tsx
new file mode 100644
index 00000000..18b215b0
--- /dev/null
+++ b/app/routes/app._index.tsx
@@ -0,0 +1,334 @@
+import { useEffect } from "react";
+import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node";
+import { useFetcher } from "@remix-run/react";
+import {
+ Page,
+ Layout,
+ Text,
+ Card,
+ Button,
+ BlockStack,
+ Box,
+ List,
+ Link,
+ InlineStack,
+} from "@shopify/polaris";
+import { TitleBar, useAppBridge } from "@shopify/app-bridge-react";
+import { authenticate } from "../shopify.server";
+
+export const loader = async ({ request }: LoaderFunctionArgs) => {
+ await authenticate.admin(request);
+
+ return null;
+};
+
+export const action = async ({ request }: ActionFunctionArgs) => {
+ const { admin } = await authenticate.admin(request);
+ const color = ["Red", "Orange", "Yellow", "Green"][
+ Math.floor(Math.random() * 4)
+ ];
+ const response = await admin.graphql(
+ `#graphql
+ mutation populateProduct($product: ProductCreateInput!) {
+ productCreate(product: $product) {
+ product {
+ id
+ title
+ handle
+ status
+ variants(first: 10) {
+ edges {
+ node {
+ id
+ price
+ barcode
+ createdAt
+ }
+ }
+ }
+ }
+ }
+ }`,
+ {
+ variables: {
+ product: {
+ title: `${color} Snowboard`,
+ },
+ },
+ },
+ );
+ const responseJson = await response.json();
+
+ const product = responseJson.data!.productCreate!.product!;
+ const variantId = product.variants.edges[0]!.node!.id!;
+
+ const variantResponse = await admin.graphql(
+ `#graphql
+ mutation shopifyRemixTemplateUpdateVariant($productId: ID!, $variants: [ProductVariantsBulkInput!]!) {
+ productVariantsBulkUpdate(productId: $productId, variants: $variants) {
+ productVariants {
+ id
+ price
+ barcode
+ createdAt
+ }
+ }
+ }`,
+ {
+ variables: {
+ productId: product.id,
+ variants: [{ id: variantId, price: "100.00" }],
+ },
+ },
+ );
+
+ const variantResponseJson = await variantResponse.json();
+
+ return {
+ product: responseJson!.data!.productCreate!.product,
+ variant:
+ variantResponseJson!.data!.productVariantsBulkUpdate!.productVariants,
+ };
+};
+
+export default function Index() {
+ const fetcher = useFetcher();
+
+ const shopify = useAppBridge();
+ const isLoading =
+ ["loading", "submitting"].includes(fetcher.state) &&
+ fetcher.formMethod === "POST";
+ const productId = fetcher.data?.product?.id.replace(
+ "gid://shopify/Product/",
+ "",
+ );
+
+ useEffect(() => {
+ if (productId) {
+ shopify.toast.show("Product created");
+ }
+ }, [productId, shopify]);
+ const generateProduct = () => fetcher.submit({}, { method: "POST" });
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ Congrats on creating a new Shopify app 🎉
+
+
+ This embedded app template uses{" "}
+
+ App Bridge
+ {" "}
+ interface examples like an{" "}
+
+ additional page in the app nav
+
+ , as well as an{" "}
+
+ Admin GraphQL
+ {" "}
+ mutation demo, to provide a starting point for app
+ development.
+
+
+
+
+ Get started with products
+
+
+ Generate a product with GraphQL and get the JSON output for
+ that product. Learn more about the{" "}
+
+ productCreate
+ {" "}
+ mutation in our API references.
+
+
+
+
+ {fetcher.data?.product && (
+
+ )}
+
+ {fetcher.data?.product && (
+ <>
+
+ {" "}
+ productCreate mutation
+
+
+
+
+ {JSON.stringify(fetcher.data.product, null, 2)}
+
+
+
+
+ {" "}
+ productVariantsBulkUpdate mutation
+
+
+
+
+ {JSON.stringify(fetcher.data.variant, null, 2)}
+
+
+
+ >
+ )}
+
+
+
+
+
+
+
+
+ App template specs
+
+
+
+
+ Framework
+
+
+ Remix
+
+
+
+
+ Database
+
+
+ Prisma
+
+
+
+
+ Interface
+
+
+
+ Polaris
+
+ {", "}
+
+ App Bridge
+
+
+
+
+
+ API
+
+
+ GraphQL API
+
+
+
+
+
+
+
+
+ Next steps
+
+
+
+ Build an{" "}
+
+ {" "}
+ example app
+ {" "}
+ to get started
+
+
+ Explore Shopify’s API with{" "}
+
+ GraphiQL
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/app/routes/app.additional.tsx b/app/routes/app.additional.tsx
new file mode 100644
index 00000000..eb9b0cfd
--- /dev/null
+++ b/app/routes/app.additional.tsx
@@ -0,0 +1,83 @@
+import {
+ Box,
+ Card,
+ Layout,
+ Link,
+ List,
+ Page,
+ Text,
+ BlockStack,
+} from "@shopify/polaris";
+import { TitleBar } from "@shopify/app-bridge-react";
+
+export default function AdditionalPage() {
+ return (
+
+
+
+
+
+
+
+ The app template comes with an additional page which
+ demonstrates how to create multiple pages within app navigation
+ using{" "}
+
+ App Bridge
+
+ .
+
+
+ To create your own page and have it show up in the app
+ navigation, add a page inside app/routes
, and a
+ link to it in the <NavMenu>
component found
+ in app/routes/app.jsx
.
+
+
+
+
+
+
+
+
+ Resources
+
+
+
+
+ App nav best practices
+
+
+
+
+
+
+
+
+ );
+}
+
+function Code({ children }: { children: React.ReactNode }) {
+ return (
+
+ {children}
+
+ );
+}
diff --git a/app/routes/app.tsx b/app/routes/app.tsx
new file mode 100644
index 00000000..bdcf1162
--- /dev/null
+++ b/app/routes/app.tsx
@@ -0,0 +1,41 @@
+import type { HeadersFunction, LoaderFunctionArgs } from "@remix-run/node";
+import { Link, Outlet, useLoaderData, useRouteError } from "@remix-run/react";
+import { boundary } from "@shopify/shopify-app-remix/server";
+import { AppProvider } from "@shopify/shopify-app-remix/react";
+import { NavMenu } from "@shopify/app-bridge-react";
+import polarisStyles from "@shopify/polaris/build/esm/styles.css?url";
+
+import { authenticate } from "../shopify.server";
+
+export const links = () => [{ rel: "stylesheet", href: polarisStyles }];
+
+export const loader = async ({ request }: LoaderFunctionArgs) => {
+ await authenticate.admin(request);
+
+ return { apiKey: process.env.SHOPIFY_API_KEY || "" };
+};
+
+export default function App() {
+ const { apiKey } = useLoaderData();
+
+ return (
+
+
+
+ Home
+
+ Additional page
+
+
+
+ );
+}
+
+// Shopify needs Remix to catch some thrown responses, so that their headers are included in the response.
+export function ErrorBoundary() {
+ return boundary.error(useRouteError());
+}
+
+export const headers: HeadersFunction = (headersArgs) => {
+ return boundary.headers(headersArgs);
+};
diff --git a/app/routes/auth.$.tsx b/app/routes/auth.$.tsx
new file mode 100644
index 00000000..8919320d
--- /dev/null
+++ b/app/routes/auth.$.tsx
@@ -0,0 +1,8 @@
+import type { LoaderFunctionArgs } from "@remix-run/node";
+import { authenticate } from "../shopify.server";
+
+export const loader = async ({ request }: LoaderFunctionArgs) => {
+ await authenticate.admin(request);
+
+ return null;
+};
diff --git a/app/routes/auth.login/error.server.tsx b/app/routes/auth.login/error.server.tsx
new file mode 100644
index 00000000..2c794974
--- /dev/null
+++ b/app/routes/auth.login/error.server.tsx
@@ -0,0 +1,16 @@
+import type { LoginError } from "@shopify/shopify-app-remix/server";
+import { LoginErrorType } from "@shopify/shopify-app-remix/server";
+
+interface LoginErrorMessage {
+ shop?: string;
+}
+
+export function loginErrorMessage(loginErrors: LoginError): LoginErrorMessage {
+ if (loginErrors?.shop === LoginErrorType.MissingShop) {
+ return { shop: "Please enter your shop domain to log in" };
+ } else if (loginErrors?.shop === LoginErrorType.InvalidShop) {
+ return { shop: "Please enter a valid shop domain to log in" };
+ }
+
+ return {};
+}
diff --git a/app/routes/auth.login/route.tsx b/app/routes/auth.login/route.tsx
new file mode 100644
index 00000000..0e9aece7
--- /dev/null
+++ b/app/routes/auth.login/route.tsx
@@ -0,0 +1,68 @@
+import { useState } from "react";
+import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node";
+import { Form, useActionData, useLoaderData } from "@remix-run/react";
+import {
+ AppProvider as PolarisAppProvider,
+ Button,
+ Card,
+ FormLayout,
+ Page,
+ Text,
+ TextField,
+} from "@shopify/polaris";
+import polarisTranslations from "@shopify/polaris/locales/en.json";
+import polarisStyles from "@shopify/polaris/build/esm/styles.css?url";
+
+import { login } from "../../shopify.server";
+
+import { loginErrorMessage } from "./error.server";
+
+export const links = () => [{ rel: "stylesheet", href: polarisStyles }];
+
+export const loader = async ({ request }: LoaderFunctionArgs) => {
+ const errors = loginErrorMessage(await login(request));
+
+ return { errors, polarisTranslations };
+};
+
+export const action = async ({ request }: ActionFunctionArgs) => {
+ const errors = loginErrorMessage(await login(request));
+
+ return {
+ errors,
+ };
+};
+
+export default function Auth() {
+ const loaderData = useLoaderData();
+ const actionData = useActionData();
+ const [shop, setShop] = useState("");
+ const { errors } = actionData || loaderData;
+
+ return (
+
+
+
+
+
+
+
+ );
+}
diff --git a/app/routes/webhooks.app.scopes_update.tsx b/app/routes/webhooks.app.scopes_update.tsx
new file mode 100644
index 00000000..c36bb64c
--- /dev/null
+++ b/app/routes/webhooks.app.scopes_update.tsx
@@ -0,0 +1,21 @@
+import type { ActionFunctionArgs } from "@remix-run/node";
+import { authenticate } from "../shopify.server";
+import db from "../db.server";
+
+export const action = async ({ request }: ActionFunctionArgs) => {
+ const { payload, session, topic, shop } = await authenticate.webhook(request);
+ console.log(`Received ${topic} webhook for ${shop}`);
+
+ const current = payload.current as string[];
+ if (session) {
+ await db.session.update({
+ where: {
+ id: session.id
+ },
+ data: {
+ scope: current.toString(),
+ },
+ });
+ }
+ return new Response();
+};
diff --git a/app/routes/webhooks.app.uninstalled.tsx b/app/routes/webhooks.app.uninstalled.tsx
new file mode 100644
index 00000000..54d3161c
--- /dev/null
+++ b/app/routes/webhooks.app.uninstalled.tsx
@@ -0,0 +1,17 @@
+import type { ActionFunctionArgs } from "@remix-run/node";
+import { authenticate } from "../shopify.server";
+import db from "../db.server";
+
+export const action = async ({ request }: ActionFunctionArgs) => {
+ const { shop, session, topic } = await authenticate.webhook(request);
+
+ console.log(`Received ${topic} webhook for ${shop}`);
+
+ // Webhook requests can trigger multiple times and after an app has already been uninstalled.
+ // If this webhook already ran, the session may have been deleted previously.
+ if (session) {
+ await db.session.deleteMany({ where: { shop } });
+ }
+
+ return new Response();
+};
diff --git a/app/shopify.server.ts b/app/shopify.server.ts
new file mode 100644
index 00000000..ec980711
--- /dev/null
+++ b/app/shopify.server.ts
@@ -0,0 +1,35 @@
+import "@shopify/shopify-app-remix/adapters/node";
+import {
+ ApiVersion,
+ AppDistribution,
+ shopifyApp,
+} from "@shopify/shopify-app-remix/server";
+import { PrismaSessionStorage } from "@shopify/shopify-app-session-storage-prisma";
+import prisma from "./db.server";
+
+const shopify = shopifyApp({
+ apiKey: process.env.SHOPIFY_API_KEY,
+ apiSecretKey: process.env.SHOPIFY_API_SECRET || "",
+ apiVersion: ApiVersion.October24,
+ scopes: process.env.SCOPES?.split(","),
+ appUrl: process.env.SHOPIFY_APP_URL || "",
+ authPathPrefix: "/auth",
+ sessionStorage: new PrismaSessionStorage(prisma),
+ distribution: AppDistribution.AppStore,
+ future: {
+ unstable_newEmbeddedAuthStrategy: true,
+ removeRest: true,
+ },
+ ...(process.env.SHOP_CUSTOM_DOMAIN
+ ? { customShopDomains: [process.env.SHOP_CUSTOM_DOMAIN] }
+ : {}),
+});
+
+export default shopify;
+export const apiVersion = ApiVersion.October24;
+export const addDocumentResponseHeaders = shopify.addDocumentResponseHeaders;
+export const authenticate = shopify.authenticate;
+export const unauthenticated = shopify.unauthenticated;
+export const login = shopify.login;
+export const registerWebhooks = shopify.registerWebhooks;
+export const sessionStorage = shopify.sessionStorage;
diff --git a/shopify.app.toml b/shopify.app.toml
index e1a27650..7215ddf9 100644
--- a/shopify.app.toml
+++ b/shopify.app.toml
@@ -10,6 +10,11 @@ api_version = "2024-10"
uri = "/webhooks/app/uninstalled"
topics = ["app/uninstalled"]
+ # Handled by: /app/routes/webhooks.app.scopes_update.tsx
+ [[webhooks.subscriptions]]
+ topics = [ "app/scopes_update" ]
+ uri = "/webhooks/app/scopes_update"
+
# Webhooks can have filters
# Only receive webhooks for product updates with a product price >= 10.00
# See: https://shopify.dev/docs/apps/build/webhooks/customize/filters
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 00000000..82142f42
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,66 @@
+import { vitePlugin as remix } from "@remix-run/dev";
+import { installGlobals } from "@remix-run/node";
+import { defineConfig, type UserConfig } from "vite";
+import tsconfigPaths from "vite-tsconfig-paths";
+
+installGlobals({ nativeFetch: true });
+
+// Related: https://github.com/remix-run/remix/issues/2835#issuecomment-1144102176
+// Replace the HOST env var with SHOPIFY_APP_URL so that it doesn't break the remix server. The CLI will eventually
+// stop passing in HOST, so we can remove this workaround after the next major release.
+if (
+ process.env.HOST &&
+ (!process.env.SHOPIFY_APP_URL ||
+ process.env.SHOPIFY_APP_URL === process.env.HOST)
+) {
+ process.env.SHOPIFY_APP_URL = process.env.HOST;
+ delete process.env.HOST;
+}
+
+const host = new URL(process.env.SHOPIFY_APP_URL || "http://localhost")
+ .hostname;
+
+let hmrConfig;
+if (host === "localhost") {
+ hmrConfig = {
+ protocol: "ws",
+ host: "localhost",
+ port: 64999,
+ clientPort: 64999,
+ };
+} else {
+ hmrConfig = {
+ protocol: "wss",
+ host: host,
+ port: parseInt(process.env.FRONTEND_PORT!) || 8002,
+ clientPort: 443,
+ };
+}
+
+export default defineConfig({
+ server: {
+ port: Number(process.env.PORT || 3000),
+ hmr: hmrConfig,
+ fs: {
+ // See https://vitejs.dev/config/server-options.html#server-fs-allow for more information
+ allow: ["app", "node_modules"],
+ },
+ },
+ plugins: [
+ remix({
+ ignoredRouteFiles: ["**/.*"],
+ future: {
+ v3_fetcherPersist: true,
+ v3_relativeSplatPath: true,
+ v3_throwAbortReason: true,
+ v3_lazyRouteDiscovery: true,
+ v3_singleFetch: false,
+ v3_routeConfig: true,
+ },
+ }),
+ tsconfigPaths(),
+ ],
+ build: {
+ assetsInlineLimit: 0,
+ },
+}) satisfies UserConfig;
From b4aab6c4bb354fb2570a04197f485305daae3d68 Mon Sep 17 00:00:00 2001
From: GitHub
Date: Wed, 18 Dec 2024 15:02:27 +0000
Subject: [PATCH 2/2] Convert template to Javascript
---
.graphqlrc.ts | 40 ---
app/db.server.ts | 15 -
app/entry.server.tsx | 59 ----
app/globals.d.ts | 1 -
app/root.tsx | 30 --
app/routes.ts | 3 -
app/routes/_index/route.tsx | 58 ----
app/routes/app._index.tsx | 334 ----------------------
app/routes/app.additional.tsx | 83 ------
app/routes/app.tsx | 41 ---
app/routes/auth.$.tsx | 8 -
app/routes/auth.login/error.server.tsx | 16 --
app/routes/auth.login/route.tsx | 68 -----
app/routes/webhooks.app.scopes_update.jsx | 22 ++
app/routes/webhooks.app.scopes_update.tsx | 21 --
app/routes/webhooks.app.uninstalled.tsx | 17 --
app/shopify.server.ts | 35 ---
vite.config.ts | 66 -----
18 files changed, 22 insertions(+), 895 deletions(-)
delete mode 100644 .graphqlrc.ts
delete mode 100644 app/db.server.ts
delete mode 100644 app/entry.server.tsx
delete mode 100644 app/globals.d.ts
delete mode 100644 app/root.tsx
delete mode 100644 app/routes.ts
delete mode 100644 app/routes/_index/route.tsx
delete mode 100644 app/routes/app._index.tsx
delete mode 100644 app/routes/app.additional.tsx
delete mode 100644 app/routes/app.tsx
delete mode 100644 app/routes/auth.$.tsx
delete mode 100644 app/routes/auth.login/error.server.tsx
delete mode 100644 app/routes/auth.login/route.tsx
create mode 100644 app/routes/webhooks.app.scopes_update.jsx
delete mode 100644 app/routes/webhooks.app.scopes_update.tsx
delete mode 100644 app/routes/webhooks.app.uninstalled.tsx
delete mode 100644 app/shopify.server.ts
delete mode 100644 vite.config.ts
diff --git a/.graphqlrc.ts b/.graphqlrc.ts
deleted file mode 100644
index ad9c5f81..00000000
--- a/.graphqlrc.ts
+++ /dev/null
@@ -1,40 +0,0 @@
-import fs from "fs";
-import { LATEST_API_VERSION } from "@shopify/shopify-api";
-import { shopifyApiProject, ApiType } from "@shopify/api-codegen-preset";
-import type { IGraphQLConfig } from "graphql-config";
-
-function getConfig() {
- const config: IGraphQLConfig = {
- projects: {
- default: shopifyApiProject({
- apiType: ApiType.Admin,
- apiVersion: LATEST_API_VERSION,
- documents: ["./app/**/*.{js,ts,jsx,tsx}", "./app/.server/**/*.{js,ts,jsx,tsx}"],
- outputDir: "./app/types",
- }),
- },
- };
-
- let extensions: string[] = [];
- try {
- extensions = fs.readdirSync("./extensions");
- } catch {
- // ignore if no extensions
- }
-
- for (const entry of extensions) {
- const extensionPath = `./extensions/${entry}`;
- const schema = `${extensionPath}/schema.graphql`;
- if (!fs.existsSync(schema)) {
- continue;
- }
- config.projects[entry] = {
- schema,
- documents: [`${extensionPath}/**/*.graphql`],
- };
- }
-
- return config;
-}
-
-module.exports = getConfig();
diff --git a/app/db.server.ts b/app/db.server.ts
deleted file mode 100644
index bafa6cc2..00000000
--- a/app/db.server.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { PrismaClient } from "@prisma/client";
-
-declare global {
- var prisma: PrismaClient;
-}
-
-if (process.env.NODE_ENV !== "production") {
- if (!global.prisma) {
- global.prisma = new PrismaClient();
- }
-}
-
-const prisma: PrismaClient = global.prisma || new PrismaClient();
-
-export default prisma;
diff --git a/app/entry.server.tsx b/app/entry.server.tsx
deleted file mode 100644
index 86274311..00000000
--- a/app/entry.server.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-import { PassThrough } from "stream";
-import { renderToPipeableStream } from "react-dom/server";
-import { RemixServer } from "@remix-run/react";
-import {
- createReadableStreamFromReadable,
- type EntryContext,
-} from "@remix-run/node";
-import { isbot } from "isbot";
-import { addDocumentResponseHeaders } from "./shopify.server";
-
-export const streamTimeout = 5000;
-
-export default async function handleRequest(
- request: Request,
- responseStatusCode: number,
- responseHeaders: Headers,
- remixContext: EntryContext
-) {
- addDocumentResponseHeaders(request, responseHeaders);
- const userAgent = request.headers.get("user-agent");
- const callbackName = isbot(userAgent ?? '')
- ? "onAllReady"
- : "onShellReady";
-
- return new Promise((resolve, reject) => {
- const { pipe, abort } = renderToPipeableStream(
- ,
- {
- [callbackName]: () => {
- const body = new PassThrough();
- const stream = createReadableStreamFromReadable(body);
-
- responseHeaders.set("Content-Type", "text/html");
- resolve(
- new Response(stream, {
- headers: responseHeaders,
- status: responseStatusCode,
- })
- );
- pipe(body);
- },
- onShellError(error) {
- reject(error);
- },
- onError(error) {
- responseStatusCode = 500;
- console.error(error);
- },
- }
- );
-
- // Automatically timeout the React renderer after 6 seconds, which ensures
- // React has enough time to flush down the rejected boundary contents
- setTimeout(abort, streamTimeout + 1000);
- });
-}
diff --git a/app/globals.d.ts b/app/globals.d.ts
deleted file mode 100644
index cbe652db..00000000
--- a/app/globals.d.ts
+++ /dev/null
@@ -1 +0,0 @@
-declare module "*.css";
diff --git a/app/root.tsx b/app/root.tsx
deleted file mode 100644
index 805f121f..00000000
--- a/app/root.tsx
+++ /dev/null
@@ -1,30 +0,0 @@
-import {
- Links,
- Meta,
- Outlet,
- Scripts,
- ScrollRestoration,
-} from "@remix-run/react";
-
-export default function App() {
- return (
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/app/routes.ts b/app/routes.ts
deleted file mode 100644
index 83892841..00000000
--- a/app/routes.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-import { flatRoutes } from "@remix-run/fs-routes";
-
-export default flatRoutes();
diff --git a/app/routes/_index/route.tsx b/app/routes/_index/route.tsx
deleted file mode 100644
index 2de9dd47..00000000
--- a/app/routes/_index/route.tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-import type { LoaderFunctionArgs } from "@remix-run/node";
-import { redirect } from "@remix-run/node";
-import { Form, useLoaderData } from "@remix-run/react";
-
-import { login } from "../../shopify.server";
-
-import styles from "./styles.module.css";
-
-export const loader = async ({ request }: LoaderFunctionArgs) => {
- const url = new URL(request.url);
-
- if (url.searchParams.get("shop")) {
- throw redirect(`/app?${url.searchParams.toString()}`);
- }
-
- return { showForm: Boolean(login) };
-};
-
-export default function App() {
- const { showForm } = useLoaderData();
-
- return (
-
-
-
A short heading about [your app]
-
- A tagline about [your app] that describes your value proposition.
-
- {showForm && (
-
- )}
-
- -
- Product feature. Some detail about your feature and
- its benefit to your customer.
-
- -
- Product feature. Some detail about your feature and
- its benefit to your customer.
-
- -
- Product feature. Some detail about your feature and
- its benefit to your customer.
-
-
-
-
- );
-}
diff --git a/app/routes/app._index.tsx b/app/routes/app._index.tsx
deleted file mode 100644
index 18b215b0..00000000
--- a/app/routes/app._index.tsx
+++ /dev/null
@@ -1,334 +0,0 @@
-import { useEffect } from "react";
-import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node";
-import { useFetcher } from "@remix-run/react";
-import {
- Page,
- Layout,
- Text,
- Card,
- Button,
- BlockStack,
- Box,
- List,
- Link,
- InlineStack,
-} from "@shopify/polaris";
-import { TitleBar, useAppBridge } from "@shopify/app-bridge-react";
-import { authenticate } from "../shopify.server";
-
-export const loader = async ({ request }: LoaderFunctionArgs) => {
- await authenticate.admin(request);
-
- return null;
-};
-
-export const action = async ({ request }: ActionFunctionArgs) => {
- const { admin } = await authenticate.admin(request);
- const color = ["Red", "Orange", "Yellow", "Green"][
- Math.floor(Math.random() * 4)
- ];
- const response = await admin.graphql(
- `#graphql
- mutation populateProduct($product: ProductCreateInput!) {
- productCreate(product: $product) {
- product {
- id
- title
- handle
- status
- variants(first: 10) {
- edges {
- node {
- id
- price
- barcode
- createdAt
- }
- }
- }
- }
- }
- }`,
- {
- variables: {
- product: {
- title: `${color} Snowboard`,
- },
- },
- },
- );
- const responseJson = await response.json();
-
- const product = responseJson.data!.productCreate!.product!;
- const variantId = product.variants.edges[0]!.node!.id!;
-
- const variantResponse = await admin.graphql(
- `#graphql
- mutation shopifyRemixTemplateUpdateVariant($productId: ID!, $variants: [ProductVariantsBulkInput!]!) {
- productVariantsBulkUpdate(productId: $productId, variants: $variants) {
- productVariants {
- id
- price
- barcode
- createdAt
- }
- }
- }`,
- {
- variables: {
- productId: product.id,
- variants: [{ id: variantId, price: "100.00" }],
- },
- },
- );
-
- const variantResponseJson = await variantResponse.json();
-
- return {
- product: responseJson!.data!.productCreate!.product,
- variant:
- variantResponseJson!.data!.productVariantsBulkUpdate!.productVariants,
- };
-};
-
-export default function Index() {
- const fetcher = useFetcher();
-
- const shopify = useAppBridge();
- const isLoading =
- ["loading", "submitting"].includes(fetcher.state) &&
- fetcher.formMethod === "POST";
- const productId = fetcher.data?.product?.id.replace(
- "gid://shopify/Product/",
- "",
- );
-
- useEffect(() => {
- if (productId) {
- shopify.toast.show("Product created");
- }
- }, [productId, shopify]);
- const generateProduct = () => fetcher.submit({}, { method: "POST" });
-
- return (
-
-
-
-
-
-
-
-
-
-
-
- Congrats on creating a new Shopify app 🎉
-
-
- This embedded app template uses{" "}
-
- App Bridge
- {" "}
- interface examples like an{" "}
-
- additional page in the app nav
-
- , as well as an{" "}
-
- Admin GraphQL
- {" "}
- mutation demo, to provide a starting point for app
- development.
-
-
-
-
- Get started with products
-
-
- Generate a product with GraphQL and get the JSON output for
- that product. Learn more about the{" "}
-
- productCreate
- {" "}
- mutation in our API references.
-
-
-
-
- {fetcher.data?.product && (
-
- )}
-
- {fetcher.data?.product && (
- <>
-
- {" "}
- productCreate mutation
-
-
-
-
- {JSON.stringify(fetcher.data.product, null, 2)}
-
-
-
-
- {" "}
- productVariantsBulkUpdate mutation
-
-
-
-
- {JSON.stringify(fetcher.data.variant, null, 2)}
-
-
-
- >
- )}
-
-
-
-
-
-
-
-
- App template specs
-
-
-
-
- Framework
-
-
- Remix
-
-
-
-
- Database
-
-
- Prisma
-
-
-
-
- Interface
-
-
-
- Polaris
-
- {", "}
-
- App Bridge
-
-
-
-
-
- API
-
-
- GraphQL API
-
-
-
-
-
-
-
-
- Next steps
-
-
-
- Build an{" "}
-
- {" "}
- example app
- {" "}
- to get started
-
-
- Explore Shopify’s API with{" "}
-
- GraphiQL
-
-
-
-
-
-
-
-
-
-
- );
-}
diff --git a/app/routes/app.additional.tsx b/app/routes/app.additional.tsx
deleted file mode 100644
index eb9b0cfd..00000000
--- a/app/routes/app.additional.tsx
+++ /dev/null
@@ -1,83 +0,0 @@
-import {
- Box,
- Card,
- Layout,
- Link,
- List,
- Page,
- Text,
- BlockStack,
-} from "@shopify/polaris";
-import { TitleBar } from "@shopify/app-bridge-react";
-
-export default function AdditionalPage() {
- return (
-
-
-
-
-
-
-
- The app template comes with an additional page which
- demonstrates how to create multiple pages within app navigation
- using{" "}
-
- App Bridge
-
- .
-
-
- To create your own page and have it show up in the app
- navigation, add a page inside app/routes
, and a
- link to it in the <NavMenu>
component found
- in app/routes/app.jsx
.
-
-
-
-
-
-
-
-
- Resources
-
-
-
-
- App nav best practices
-
-
-
-
-
-
-
-
- );
-}
-
-function Code({ children }: { children: React.ReactNode }) {
- return (
-
- {children}
-
- );
-}
diff --git a/app/routes/app.tsx b/app/routes/app.tsx
deleted file mode 100644
index bdcf1162..00000000
--- a/app/routes/app.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import type { HeadersFunction, LoaderFunctionArgs } from "@remix-run/node";
-import { Link, Outlet, useLoaderData, useRouteError } from "@remix-run/react";
-import { boundary } from "@shopify/shopify-app-remix/server";
-import { AppProvider } from "@shopify/shopify-app-remix/react";
-import { NavMenu } from "@shopify/app-bridge-react";
-import polarisStyles from "@shopify/polaris/build/esm/styles.css?url";
-
-import { authenticate } from "../shopify.server";
-
-export const links = () => [{ rel: "stylesheet", href: polarisStyles }];
-
-export const loader = async ({ request }: LoaderFunctionArgs) => {
- await authenticate.admin(request);
-
- return { apiKey: process.env.SHOPIFY_API_KEY || "" };
-};
-
-export default function App() {
- const { apiKey } = useLoaderData();
-
- return (
-
-
-
- Home
-
- Additional page
-
-
-
- );
-}
-
-// Shopify needs Remix to catch some thrown responses, so that their headers are included in the response.
-export function ErrorBoundary() {
- return boundary.error(useRouteError());
-}
-
-export const headers: HeadersFunction = (headersArgs) => {
- return boundary.headers(headersArgs);
-};
diff --git a/app/routes/auth.$.tsx b/app/routes/auth.$.tsx
deleted file mode 100644
index 8919320d..00000000
--- a/app/routes/auth.$.tsx
+++ /dev/null
@@ -1,8 +0,0 @@
-import type { LoaderFunctionArgs } from "@remix-run/node";
-import { authenticate } from "../shopify.server";
-
-export const loader = async ({ request }: LoaderFunctionArgs) => {
- await authenticate.admin(request);
-
- return null;
-};
diff --git a/app/routes/auth.login/error.server.tsx b/app/routes/auth.login/error.server.tsx
deleted file mode 100644
index 2c794974..00000000
--- a/app/routes/auth.login/error.server.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import type { LoginError } from "@shopify/shopify-app-remix/server";
-import { LoginErrorType } from "@shopify/shopify-app-remix/server";
-
-interface LoginErrorMessage {
- shop?: string;
-}
-
-export function loginErrorMessage(loginErrors: LoginError): LoginErrorMessage {
- if (loginErrors?.shop === LoginErrorType.MissingShop) {
- return { shop: "Please enter your shop domain to log in" };
- } else if (loginErrors?.shop === LoginErrorType.InvalidShop) {
- return { shop: "Please enter a valid shop domain to log in" };
- }
-
- return {};
-}
diff --git a/app/routes/auth.login/route.tsx b/app/routes/auth.login/route.tsx
deleted file mode 100644
index 0e9aece7..00000000
--- a/app/routes/auth.login/route.tsx
+++ /dev/null
@@ -1,68 +0,0 @@
-import { useState } from "react";
-import type { ActionFunctionArgs, LoaderFunctionArgs } from "@remix-run/node";
-import { Form, useActionData, useLoaderData } from "@remix-run/react";
-import {
- AppProvider as PolarisAppProvider,
- Button,
- Card,
- FormLayout,
- Page,
- Text,
- TextField,
-} from "@shopify/polaris";
-import polarisTranslations from "@shopify/polaris/locales/en.json";
-import polarisStyles from "@shopify/polaris/build/esm/styles.css?url";
-
-import { login } from "../../shopify.server";
-
-import { loginErrorMessage } from "./error.server";
-
-export const links = () => [{ rel: "stylesheet", href: polarisStyles }];
-
-export const loader = async ({ request }: LoaderFunctionArgs) => {
- const errors = loginErrorMessage(await login(request));
-
- return { errors, polarisTranslations };
-};
-
-export const action = async ({ request }: ActionFunctionArgs) => {
- const errors = loginErrorMessage(await login(request));
-
- return {
- errors,
- };
-};
-
-export default function Auth() {
- const loaderData = useLoaderData();
- const actionData = useActionData();
- const [shop, setShop] = useState("");
- const { errors } = actionData || loaderData;
-
- return (
-
-
-
-
-
-
-
- );
-}
diff --git a/app/routes/webhooks.app.scopes_update.jsx b/app/routes/webhooks.app.scopes_update.jsx
new file mode 100644
index 00000000..a40946ea
--- /dev/null
+++ b/app/routes/webhooks.app.scopes_update.jsx
@@ -0,0 +1,22 @@
+import { authenticate } from "../shopify.server";
+import db from "../db.server";
+
+export const action = async ({ request }) => {
+ const { payload, session, topic, shop } = await authenticate.webhook(request);
+
+ console.log(`Received ${topic} webhook for ${shop}`);
+ const current = payload.current;
+
+ if (session) {
+ await db.session.update({
+ where: {
+ id: session.id,
+ },
+ data: {
+ scope: current.toString(),
+ },
+ });
+ }
+
+ return new Response();
+};
diff --git a/app/routes/webhooks.app.scopes_update.tsx b/app/routes/webhooks.app.scopes_update.tsx
deleted file mode 100644
index c36bb64c..00000000
--- a/app/routes/webhooks.app.scopes_update.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import type { ActionFunctionArgs } from "@remix-run/node";
-import { authenticate } from "../shopify.server";
-import db from "../db.server";
-
-export const action = async ({ request }: ActionFunctionArgs) => {
- const { payload, session, topic, shop } = await authenticate.webhook(request);
- console.log(`Received ${topic} webhook for ${shop}`);
-
- const current = payload.current as string[];
- if (session) {
- await db.session.update({
- where: {
- id: session.id
- },
- data: {
- scope: current.toString(),
- },
- });
- }
- return new Response();
-};
diff --git a/app/routes/webhooks.app.uninstalled.tsx b/app/routes/webhooks.app.uninstalled.tsx
deleted file mode 100644
index 54d3161c..00000000
--- a/app/routes/webhooks.app.uninstalled.tsx
+++ /dev/null
@@ -1,17 +0,0 @@
-import type { ActionFunctionArgs } from "@remix-run/node";
-import { authenticate } from "../shopify.server";
-import db from "../db.server";
-
-export const action = async ({ request }: ActionFunctionArgs) => {
- const { shop, session, topic } = await authenticate.webhook(request);
-
- console.log(`Received ${topic} webhook for ${shop}`);
-
- // Webhook requests can trigger multiple times and after an app has already been uninstalled.
- // If this webhook already ran, the session may have been deleted previously.
- if (session) {
- await db.session.deleteMany({ where: { shop } });
- }
-
- return new Response();
-};
diff --git a/app/shopify.server.ts b/app/shopify.server.ts
deleted file mode 100644
index ec980711..00000000
--- a/app/shopify.server.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import "@shopify/shopify-app-remix/adapters/node";
-import {
- ApiVersion,
- AppDistribution,
- shopifyApp,
-} from "@shopify/shopify-app-remix/server";
-import { PrismaSessionStorage } from "@shopify/shopify-app-session-storage-prisma";
-import prisma from "./db.server";
-
-const shopify = shopifyApp({
- apiKey: process.env.SHOPIFY_API_KEY,
- apiSecretKey: process.env.SHOPIFY_API_SECRET || "",
- apiVersion: ApiVersion.October24,
- scopes: process.env.SCOPES?.split(","),
- appUrl: process.env.SHOPIFY_APP_URL || "",
- authPathPrefix: "/auth",
- sessionStorage: new PrismaSessionStorage(prisma),
- distribution: AppDistribution.AppStore,
- future: {
- unstable_newEmbeddedAuthStrategy: true,
- removeRest: true,
- },
- ...(process.env.SHOP_CUSTOM_DOMAIN
- ? { customShopDomains: [process.env.SHOP_CUSTOM_DOMAIN] }
- : {}),
-});
-
-export default shopify;
-export const apiVersion = ApiVersion.October24;
-export const addDocumentResponseHeaders = shopify.addDocumentResponseHeaders;
-export const authenticate = shopify.authenticate;
-export const unauthenticated = shopify.unauthenticated;
-export const login = shopify.login;
-export const registerWebhooks = shopify.registerWebhooks;
-export const sessionStorage = shopify.sessionStorage;
diff --git a/vite.config.ts b/vite.config.ts
deleted file mode 100644
index 82142f42..00000000
--- a/vite.config.ts
+++ /dev/null
@@ -1,66 +0,0 @@
-import { vitePlugin as remix } from "@remix-run/dev";
-import { installGlobals } from "@remix-run/node";
-import { defineConfig, type UserConfig } from "vite";
-import tsconfigPaths from "vite-tsconfig-paths";
-
-installGlobals({ nativeFetch: true });
-
-// Related: https://github.com/remix-run/remix/issues/2835#issuecomment-1144102176
-// Replace the HOST env var with SHOPIFY_APP_URL so that it doesn't break the remix server. The CLI will eventually
-// stop passing in HOST, so we can remove this workaround after the next major release.
-if (
- process.env.HOST &&
- (!process.env.SHOPIFY_APP_URL ||
- process.env.SHOPIFY_APP_URL === process.env.HOST)
-) {
- process.env.SHOPIFY_APP_URL = process.env.HOST;
- delete process.env.HOST;
-}
-
-const host = new URL(process.env.SHOPIFY_APP_URL || "http://localhost")
- .hostname;
-
-let hmrConfig;
-if (host === "localhost") {
- hmrConfig = {
- protocol: "ws",
- host: "localhost",
- port: 64999,
- clientPort: 64999,
- };
-} else {
- hmrConfig = {
- protocol: "wss",
- host: host,
- port: parseInt(process.env.FRONTEND_PORT!) || 8002,
- clientPort: 443,
- };
-}
-
-export default defineConfig({
- server: {
- port: Number(process.env.PORT || 3000),
- hmr: hmrConfig,
- fs: {
- // See https://vitejs.dev/config/server-options.html#server-fs-allow for more information
- allow: ["app", "node_modules"],
- },
- },
- plugins: [
- remix({
- ignoredRouteFiles: ["**/.*"],
- future: {
- v3_fetcherPersist: true,
- v3_relativeSplatPath: true,
- v3_throwAbortReason: true,
- v3_lazyRouteDiscovery: true,
- v3_singleFetch: false,
- v3_routeConfig: true,
- },
- }),
- tsconfigPaths(),
- ],
- build: {
- assetsInlineLimit: 0,
- },
-}) satisfies UserConfig;