diff --git a/.prettierrc b/.prettierrc
index 70c334c..b9a8520 100644
--- a/.prettierrc
+++ b/.prettierrc
@@ -1,6 +1,6 @@
{
"trailingComma": "es5",
- "tabWidth": 4,
+ "tabWidth": 2,
"semi": false,
"singleQuote": false,
"overrides": [
diff --git a/.swcrc b/.swcrc
index 8c48af1..5b29ac2 100644
--- a/.swcrc
+++ b/.swcrc
@@ -1,31 +1,31 @@
{
- "jsc": {
- "target": "es2022",
- "parser": {
- "syntax": "typescript",
- "tsx": true,
- "decorators": false,
- "dynamicImport": false
- },
- "transform": {
- "react": {
- "pragma": "React.createElement",
- "pragmaFrag": "React.Fragment",
- "throwIfNamespace": true,
- "development": false,
- "useBuiltins": false,
- "runtime": "automatic"
- },
- "hidden": {
- "jest": true
- }
- }
+ "jsc": {
+ "target": "es2022",
+ "parser": {
+ "syntax": "typescript",
+ "tsx": true,
+ "decorators": false,
+ "dynamicImport": false
},
- "module": {
- "type": "commonjs",
- "strict": false,
- "strictMode": true,
- "lazy": false,
- "noInterop": false
+ "transform": {
+ "react": {
+ "pragma": "React.createElement",
+ "pragmaFrag": "React.Fragment",
+ "throwIfNamespace": true,
+ "development": false,
+ "useBuiltins": false,
+ "runtime": "automatic"
+ },
+ "hidden": {
+ "jest": true
+ }
}
+ },
+ "module": {
+ "type": "commonjs",
+ "strict": false,
+ "strictMode": true,
+ "lazy": false,
+ "noInterop": false
+ }
}
\ No newline at end of file
diff --git a/app/entry.client.tsx b/app/entry.client.tsx
index 94d5dc0..8f13e78 100644
--- a/app/entry.client.tsx
+++ b/app/entry.client.tsx
@@ -4,9 +4,10 @@
* For more information, see https://remix.run/file-conventions/entry.client
*/
-import { RemixBrowser } from "@remix-run/react";
-import { startTransition, StrictMode } from "react";
-import { hydrateRoot } from "react-dom/client";
+import { RemixBrowser } from "@remix-run/react"
+import React from "react"
+import { startTransition, StrictMode } from "react"
+import { hydrateRoot } from "react-dom/client"
startTransition(() => {
hydrateRoot(
@@ -14,5 +15,5 @@ startTransition(() => {
- );
-});
+ )
+})
diff --git a/app/entry.server.tsx b/app/entry.server.tsx
index c2fd369..a119799 100644
--- a/app/entry.server.tsx
+++ b/app/entry.server.tsx
@@ -4,16 +4,16 @@
* For more information, see https://remix.run/file-conventions/entry.server
*/
-import { PassThrough } from "node:stream";
+import { PassThrough } from "node:stream"
-import type { AppLoadContext, EntryContext } from "@remix-run/node";
-import { createReadableStreamFromReadable } from "@remix-run/node";
-import { RemixServer } from "@remix-run/react";
-import { isbot } from "isbot";
-import { renderToPipeableStream } from "react-dom/server";
-import React from "react";
+import type { AppLoadContext, EntryContext } from "@remix-run/node"
+import { createReadableStreamFromReadable } from "@remix-run/node"
+import { RemixServer } from "@remix-run/react"
+import { isbot } from "isbot"
+import { renderToPipeableStream } from "react-dom/server"
+import React from "react"
-const ABORT_DELAY = 5_000;
+const ABORT_DELAY = 5_000
export default function handleRequest(
request: Request,
@@ -37,7 +37,7 @@ export default function handleRequest(
responseStatusCode,
responseHeaders,
remixContext
- );
+ )
}
function handleBotRequest(
@@ -47,7 +47,7 @@ function handleBotRequest(
remixContext: EntryContext
) {
return new Promise((resolve, reject) => {
- let shellRendered = false;
+ let shellRendered = false
const { pipe, abort } = renderToPipeableStream(
,
{
onAllReady() {
- shellRendered = true;
- const body = new PassThrough();
- const stream = createReadableStreamFromReadable(body);
+ shellRendered = true
+ const body = new PassThrough()
+ const stream = createReadableStreamFromReadable(body)
- responseHeaders.set("Content-Type", "text/html");
+ responseHeaders.set("Content-Type", "text/html")
resolve(
new Response(stream, {
headers: responseHeaders,
status: responseStatusCode,
})
- );
+ )
- pipe(body);
+ pipe(body)
},
onShellError(error: unknown) {
- reject(error);
+ reject(error)
},
onError(error: unknown) {
- responseStatusCode = 500;
+ responseStatusCode = 500
// Log streaming rendering errors from inside the shell. Don't log
// errors encountered during initial shell rendering since they'll
// reject and get logged in handleDocumentRequest.
if (shellRendered) {
- console.error(error);
+ console.error(error)
}
},
}
- );
+ )
- setTimeout(abort, ABORT_DELAY);
- });
+ setTimeout(abort, ABORT_DELAY)
+ })
}
function handleBrowserRequest(
@@ -97,7 +97,7 @@ function handleBrowserRequest(
remixContext: EntryContext
) {
return new Promise((resolve, reject) => {
- let shellRendered = false;
+ let shellRendered = false
const { pipe, abort } = renderToPipeableStream(
,
{
onShellReady() {
- shellRendered = true;
- const body = new PassThrough();
- const stream = createReadableStreamFromReadable(body);
+ shellRendered = true
+ const body = new PassThrough()
+ const stream = createReadableStreamFromReadable(body)
- responseHeaders.set("Content-Type", "text/html");
+ responseHeaders.set("Content-Type", "text/html")
resolve(
new Response(stream, {
headers: responseHeaders,
status: responseStatusCode,
})
- );
+ )
- pipe(body);
+ pipe(body)
},
onShellError(error: unknown) {
- reject(error);
+ reject(error)
},
onError(error: unknown) {
- responseStatusCode = 500;
+ responseStatusCode = 500
// Log streaming rendering errors from inside the shell. Don't log
// errors encountered during initial shell rendering since they'll
// reject and get logged in handleDocumentRequest.
if (shellRendered) {
- console.error(error);
+ console.error(error)
}
},
}
- );
+ )
- setTimeout(abort, ABORT_DELAY);
- });
+ setTimeout(abort, ABORT_DELAY)
+ })
}
diff --git a/app/entry.worker.ts b/app/entry.worker.ts
index c6e7d9a..10fa180 100644
--- a/app/entry.worker.ts
+++ b/app/entry.worker.ts
@@ -1,17 +1,17 @@
///
-export {};
+export {}
-declare let self: ServiceWorkerGlobalScope;
+declare let self: ServiceWorkerGlobalScope
-self.addEventListener('install', event => {
- console.log('Service worker installed');
+self.addEventListener("install", (event) => {
+ console.log("Service worker installed")
- event.waitUntil(self.skipWaiting());
-});
+ event.waitUntil(self.skipWaiting())
+})
-self.addEventListener('activate', event => {
- console.log('Service worker activated');
+self.addEventListener("activate", (event) => {
+ console.log("Service worker activated")
- event.waitUntil(self.clients.claim());
-});
+ event.waitUntil(self.clients.claim())
+})
diff --git a/app/root.test.tsx b/app/root.test.tsx
index a148c7c..f9ccd48 100644
--- a/app/root.test.tsx
+++ b/app/root.test.tsx
@@ -1,45 +1,47 @@
-import { act, render, screen, waitFor } from '@testing-library/react'
-import MatchMediaMock from 'jest-matchmedia-mock';
+import { act, render, screen, waitFor } from "@testing-library/react"
+import MatchMediaMock from "jest-matchmedia-mock"
import { createRemixStub, RemixStubProps } from "@remix-run/testing"
-import App from './root'
-import React from 'react'
+import App from "./root"
+import React from "react"
let RemixStub: (props: RemixStubProps) => React.JSX.Element
describe("root", () => {
- beforeEach(() => {
- RemixStub = createRemixStub([
- {
- path: "/",
- meta: () => ([]),
- links: () => ([]),
- loader: () => ({ isSupported: true}),
- Component: App
- }
- ])
- })
+ beforeEach(() => {
+ RemixStub = createRemixStub([
+ {
+ path: "/",
+ meta: () => [],
+ links: () => [],
+ loader: () => ({ isSupported: true }),
+ Component: App,
+ },
+ ])
+ })
- let matchMedia: MatchMediaMock
+ let matchMedia: MatchMediaMock
- beforeAll(() => {
- matchMedia = new MatchMediaMock();
- })
-
- afterEach(() => {
- matchMedia.clear();
- })
+ beforeAll(() => {
+ matchMedia = new MatchMediaMock()
+ })
- describe("when user is on iOS", () => {
- describe("version 16.4 or higher", () => {
- describe("and tack up now is not already installed on their device", () => {
- test("they are instructed to install tack up now", async () => {
- render()
+ afterEach(() => {
+ matchMedia.clear()
+ })
- await waitFor(() => screen.findByText(/Install/), { timeout: 2000 })
+ describe("when user is on iOS", () => {
+ describe("version 16.4 or higher", () => {
+ describe("and tack up now is not already installed on their device", () => {
+ test("they are instructed to install tack up now", async () => {
+ render()
- expect(true).toBe(true)
- })
- })
+ await waitFor(() => screen.findByText(/Install/), {
+ timeout: 2000,
+ })
+
+ expect(true).toBe(true)
})
+ })
})
+ })
})
diff --git a/app/root.tsx b/app/root.tsx
index 5e2f692..edb223f 100644
--- a/app/root.tsx
+++ b/app/root.tsx
@@ -1,33 +1,30 @@
import {
- Links,
- Meta,
- Outlet,
- Scripts,
- ScrollRestoration,
+ Links,
+ Meta,
+ Outlet,
+ Scripts,
+ ScrollRestoration,
} from "@remix-run/react"
import React from "react"
export function Layout({ children }: { children: React.ReactNode }) {
- return (
-
-
-
-
-
-
-
-
- {children}
-
-
-
-
- )
+ return (
+
+
+
+
+
+
+
+
+ {children}
+
+
+
+
+ )
}
export default function App() {
- return
+ return
}
diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx
index aebee19..0f153da 100644
--- a/app/routes/_index.tsx
+++ b/app/routes/_index.tsx
@@ -1,52 +1,82 @@
-import { json, type LoaderFunctionArgs, type MetaFunction } from "@remix-run/node";
-import { useLoaderData } from "@remix-run/react";
-import React, { Suspense, useEffect } from "react";
-import UAParser from "ua-parser-js"
-import versionAtLeast from "semver/functions/gte"
+import {
+ json,
+ type LoaderFunctionArgs,
+ type MetaFunction,
+} from "@remix-run/node"
+import { useLoaderData } from "@remix-run/react"
+import React, { useState } from "react"
import coerceSemver from "semver/functions/coerce"
+import versionAtLeast from "semver/functions/gte"
+import UAParser from "ua-parser-js"
+import { usePush } from "remix-pwa-monorepo/packages/push/client/hook"
export const meta: MetaFunction = () => {
return [
{ title: "Tack Up Now!" },
{ name: "description", content: "Get equinelive notifications" },
- ];
-};
+ ]
+}
export const loader = async ({ request }: LoaderFunctionArgs) => {
const userAgent = request.headers.get("user-agent")
const os = new UAParser(userAgent ?? "").getOS()
const isSupported =
- os.name !== "iOS" ||
- versionAtLeast(coerceSemver(os.version) ?? "0.0.0", "16.4.0")
+ os.name !== "iOS" ||
+ versionAtLeast(coerceSemver(os.version) ?? "0.0.0", "16.4.0")
return json({ isSupported })
}
export default function Index() {
-
const { isSupported } = useLoaderData()
return (
-
-
-
+
- );
+ )
}
-
function LandingMessage({ isSupported }: { isSupported: boolean }) {
+ const isClient = typeof window !== "undefined"
+ const notificationsEnabled =
+ isClient &&
+ "Notification" in window &&
+ window.Notification.permission === "granted"
- if (typeof window === "undefined") return Loading
+ const isRunningPWA =
+ isClient &&
+ (("standalone" in navigator && (navigator.standalone as boolean)) ||
+ matchMedia("(dislay-mode: standalone)").matches)
- const isRunningPWA = ("standalone" in navigator && navigator.standalone as boolean) || matchMedia("(dislay-mode: standalone)").matches
- const notificationsEnabled = "Notification" in window && window.Notification.permission === "granted"
- const message = isRunningPWA && !notificationsEnabled
- ? "Enable notifications"
- : isSupported
- ? "Install Tack Up Now!"
- : "Sorry, your device doesn't support Tack Up Now! :("
+ const [isInstalled, setIsInstalled] = useState(notificationsEnabled)
- return
+ return !isClient ? (
+ Loading
+ ) : isInstalled ? (
+ Your Notifications
+ ) : isRunningPWA && !notificationsEnabled ? (
+ setIsInstalled(true)} />
+ ) : isSupported ? (
+ "Install Tack Up Now!"
+ ) : (
+ "Sorry, your device doesn't support Tack Up Now! :("
+ )
+}
+
+function EnableButton({ onSubscribe }: { onSubscribe: () => void }) {
+ const { subscribeToPush, requestPermission } = usePush()
+
+ return
+
+ async function subscribe() {
+ console.log("Hey the thing was clicked wow")
+ subscribeToPush("Derpderp", (subscription) => {
+ fetch("/subscribe", {
+ method: "POST",
+ body: JSON.stringify(subscription),
+ })
+ onSubscribe()
+ })
+ }
}
diff --git a/e2e/install.spec.ts b/e2e/install.spec.ts
index 305e265..3a86133 100644
--- a/e2e/install.spec.ts
+++ b/e2e/install.spec.ts
@@ -1,76 +1,186 @@
-import { test, expect } from "@playwright/test"
+import { test, expect, Page } from "@playwright/test"
+import { before } from "node:test"
const { describe, beforeEach, skip, use } = test
function webkitOnly() {
- beforeEach(async ({ browserName }) => {
- if (browserName !== "webkit") skip()
- })
+ beforeEach(async ({ browserName }) => {
+ if (browserName !== "webkit") skip()
+ })
}
describe("when user is on iOS", () => {
- webkitOnly()
+ webkitOnly()
- describe("version 16.4 or higher", () => {
- use({
- userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 16_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Mobile/15E148 Safari/604.1",
- })
-
- test("and tack up now is not running as a PWA, they are instructed to install tack up now", async ({ page }) => {
- await page.goto("/")
-
- const installText = await page.getByText(/Install Tack Up Now!/)
-
- await expect(installText).toBeAttached()
- })
-
- describe("and tack up now is running as a PWA", () => {
-
- beforeEach(async ({ page }) => {
- await page.addInitScript(() => (window.navigator as any)["standalone"] = true)
- })
-
- test("and notifications aren't enabled, they are asked to enable notifications", async ({ page, browser }) => {
- await page.addInitScript(() => (Object.defineProperty(window.Notification, "permission", {value: "default", writable: true })))
- await page.goto("/")
-
- const notificationText = await page.getByText(/Enable notifications/)
-
- await expect(notificationText).toBeAttached()
- })
-
- describe("and notifications are enabled", () => {
-
- beforeEach(async ({ page }) => {
- await page.addInitScript(() => {
- window.Notification = window.Notification ?? {}
- Object.defineProperty(window.Notification, "permission", {value: "granted", writable: true })
- })
- })
-
- test("they aren't asked to enable notifications", async ({ page }) => {
- await page.goto("/")
- await page.evaluate(async () => await navigator.serviceWorker.ready)
-
- const notificationText = await page.getByText(/Enable notifications/)
-
- await expect(notificationText).not.toBeAttached()
- })
- })
- })
+ describe("version 16.4 or higher", () => {
+ use({
+ userAgent:
+ "Mozilla/5.0 (iPhone; CPU iPhone OS 16_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Mobile/15E148 Safari/604.1",
})
- describe("version 16.3 and under", () => {
- use({
- userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 16_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Mobile/15E148 Safari/604.1",
+ test("and tack up now is not running as a PWA, they are instructed to install tack up now", async ({
+ page,
+ }) => {
+ await page.goto("/")
+
+ const installText = await page.getByText(/Install Tack Up Now!/)
+
+ await expect(installText).toBeAttached()
+ })
+
+ describe("and tack up now is running as a PWA", () => {
+ beforeEach(async ({ page }) => {
+ await page.addInitScript(
+ () => ((window.navigator as any)["standalone"] = true)
+ )
+ await stubServiceWorker(page)
+ })
+
+ describe("and notifications aren't enabled", () => {
+ test("they are asked to enable notifications", async ({ page }) => {
+ await stubNotifications(page, { permission: "default" })
+ await page.goto("/")
+
+ const enableButton = await page.getByText(/Enable notifications/)
+
+ await expect(enableButton).toBeAttached()
})
- test("version 16.3 and under they are informed that their iOS version isn't supported", async ({ page }) => {
+ describe("and then the user enables notifications", () => {
+ beforeEach(async ({ page }) => {
+ await stubNotifications(page, {
+ permission: "default",
+ requestPermissionResult: "granted",
+ })
+ await page.route("/subscribe", async (route) => {
+ await route.fulfill()
+ })
+ })
+
+ test("their push token is submitted after notifications are enabled", async ({
+ page,
+ }) => {
await page.goto("/")
- const sorryText = await page.getByText(/Sorry/)
+ const requestPromise = page.waitForRequest("/subscribe")
+ await page.getByText(/Enable notifications/).click()
+ const request = await requestPromise
- await expect(sorryText).toBeVisible()
+ await expect(request.postDataJSON()).toEqual({ hi: "subscription" })
+ })
+
+ test("users see tack up now after enabling notifications", async ({
+ page,
+ }) => {
+ await page.goto("/")
+
+ const requestPromise = page.waitForRequest("/subscribe")
+ await page.getByText(/Enable notifications/).click()
+ await requestPromise
+
+ const yourNotificationsHeading =
+ await page.getByText(/Your Notifications/)
+
+ await expect(yourNotificationsHeading).toBeVisible()
+ })
})
+ })
+
+ describe("and notifications are enabled", () => {
+ beforeEach(async ({ page }) => {
+ await stubNotifications(page, { permission: "granted" })
+ })
+
+ test("they aren't asked to enable notifications", async ({ page }) => {
+ await page.goto("/")
+ await page.evaluate(async () => await navigator.serviceWorker.ready)
+
+ const notificationText = await page.getByText(/Enable notifications/)
+
+ await expect(notificationText).not.toBeAttached()
+ })
+
+ test("users see tack up now", async ({ page }) => {
+ await page.goto("/")
+
+ const yourNotificationsHeading =
+ await page.getByText(/Your Notifications/)
+
+ await expect(yourNotificationsHeading).toBeVisible()
+ })
+ })
})
+ })
+
+ describe("version 16.3 and under", () => {
+ use({
+ userAgent:
+ "Mozilla/5.0 (iPhone; CPU iPhone OS 16_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.6 Mobile/15E148 Safari/604.1",
+ })
+
+ test("version 16.3 and under they are informed that their iOS version isn't supported", async ({
+ page,
+ }) => {
+ await page.goto("/")
+
+ const sorryText = await page.getByText(/Sorry/)
+
+ await expect(sorryText).toBeVisible()
+ })
+ })
})
+
+function stubNotifications(
+ page: Page,
+ args: {
+ permission: NotificationPermission
+ requestPermissionResult?: NotificationPermission
+ }
+) {
+ return page.addInitScript(
+ (args: {
+ permission: NotificationPermission
+ requestPermissionResult?: NotificationPermission
+ }) => {
+ window.Notification = window.Notification ?? {}
+ Object.defineProperty(window.Notification, "permission", {
+ value: args.permission,
+ writable: true,
+ })
+ Object.defineProperty(window.Notification, "requestPermission", {
+ value: () =>
+ Promise.resolve(args.requestPermissionResult ?? args.permission),
+ })
+ },
+ args
+ )
+}
+
+function stubServiceWorker(page: Page) {
+ return page.addInitScript(() => {
+ const registration = {
+ pushManager: {
+ getSubscription() {
+ return Promise.resolve({ hi: "subscription" })
+ },
+ subscribe(args: Parameters[0]) {
+ return Promise.resolve({ hi: "subscription" })
+ },
+ },
+ }
+
+ Object.defineProperty(navigator, "serviceWorker", {
+ value: {
+ register() {
+ return Promise.resolve(registration)
+ },
+ getRegistration() {
+ return Promise.resolve(registration)
+ },
+ addEventListener() {},
+ removeEventListener() {},
+ },
+ writable: false,
+ })
+ })
+}
diff --git a/package-lock.json b/package-lock.json
index af9ffee..dd9b890 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -16,6 +16,7 @@
"isbot": "^4.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "remix-pwa-monorepo": "github:remix-pwa/monorepo#main",
"remix-utils": "^7.6.0",
"semver": "^7.6.3",
"ua-parser-js": "^1.0.39"
@@ -14603,6 +14604,25 @@
"url": "https://opencollective.com/unified"
}
},
+ "node_modules/remix-pwa-monorepo": {
+ "resolved": "git+ssh://git@github.com/remix-pwa/monorepo.git#dda9d68b1c69642679d6ff17658f21fe24c668d6",
+ "license": "MIT",
+ "workspaces": [
+ "packages/cli",
+ "packages/client",
+ "packages/dev",
+ "packages/eslint-config",
+ "packages/lint-staged-config",
+ "packages/push",
+ "packages/sw",
+ "packages/sync",
+ "packages/worker-runtime",
+ "playground"
+ ],
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
"node_modules/remix-utils": {
"version": "7.6.0",
"resolved": "https://registry.npmjs.org/remix-utils/-/remix-utils-7.6.0.tgz",
diff --git a/package.json b/package.json
index da54dc4..059663d 100644
--- a/package.json
+++ b/package.json
@@ -23,6 +23,7 @@
"isbot": "^4.1.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
+ "remix-pwa-monorepo": "github:remix-pwa/monorepo#main",
"remix-utils": "^7.6.0",
"semver": "^7.6.3",
"ua-parser-js": "^1.0.39"
@@ -74,4 +75,4 @@
"engines": {
"node": ">=18.0.0"
}
-}
+}
\ No newline at end of file
diff --git a/server.ts b/server.ts
index ac711c1..f368ead 100644
--- a/server.ts
+++ b/server.ts
@@ -11,10 +11,6 @@ app.get("/api", (req, res) => res.send("HI"))
// and your app is "just a request handler"
app.all("*", createRequestHandler({ build }))
-import { register } from "register-service-worker";
-
-register(`/service-worker.js`)
-
app.listen(3000, () => {
console.log("App listening on http://localhost:3000")
})
diff --git a/transform.js b/transform.js
index b60b3c8..7a87bb0 100644
--- a/transform.js
+++ b/transform.js
@@ -12,13 +12,13 @@ import baseConfig from "./thebabel.config.cjs"
let metaPlugin = ({ types: t }) => ({
visitor: {
MetaProperty: (path) => {
- path.replaceWith(t.identifier("undefined"));
+ path.replaceWith(t.identifier("undefined"))
},
},
-});
+})
export default babelJest.createTransformer({
babelrc: false,
...baseConfig,
plugins: [...baseConfig.plugins, metaPlugin],
-});
\ No newline at end of file
+})
diff --git a/vite.config.ts b/vite.config.ts
index 53183cb..3715143 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -2,14 +2,10 @@ import { vitePlugin as remix } from "@remix-run/dev"
import { installGlobals } from "@remix-run/node"
import { defineConfig } from "vite"
import tsconfigPaths from "vite-tsconfig-paths"
-import { remixPWA } from '@remix-pwa/dev'
+import { remixPWA } from "@remix-pwa/dev"
installGlobals()
export default defineConfig({
- plugins: [
- remix(),
- tsconfigPaths(),
- remixPWA()
- ],
+ plugins: [remix(), tsconfigPaths(), remixPWA()],
})