diff --git a/packages/sw/src/components/ManifestLink.tsx b/packages/sw/src/components/ManifestLink.tsx index 109b4e56..3fbe8b48 100644 --- a/packages/sw/src/components/ManifestLink.tsx +++ b/packages/sw/src/components/ManifestLink.tsx @@ -1,5 +1,5 @@ import React from 'react'; export const ManifestLink = ({ manifestUrl = '/manifest.webmanifest' }: { manifestUrl?: string }) => { - return ; + return ; }; diff --git a/playground/public/entry.worker.js b/playground/public/entry.worker.js index bd0006b6..a49102c8 100644 --- a/playground/public/entry.worker.js +++ b/playground/public/entry.worker.js @@ -4969,7 +4969,6 @@ class PushManager { console.error("Notification error:", event); } } -self.logger = logger; console.log("Hello from service worker!"); console.log("development", "https://api.example.com", "value"); const documentCache = new EnhancedCache("document-cache", { @@ -5000,6 +4999,12 @@ const isAssetRequest = (request) => { return self.__workerManifest.assets.includes(url.pathname) && hasNoParams; }; const defaultFetchHandler = async ({ request, context }) => { + const req = context.event.request; + req.referrer; + req.headers.get("referer"); + if (req.destination === "image") { + console.log("Image request", req); + } if (isAssetRequest(request)) { return assetCache.handleRequest(request); } @@ -5032,63 +5037,107 @@ const entryWorker = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineP defaultFetchHandler, getLoadContext }, Symbol.toStringTag, { value: "Module" })); -var __getOwnPropNames$2 = Object.getOwnPropertyNames; -var __commonJS$2 = (cb, mod) => function __require() { - return mod || (0, cb[__getOwnPropNames$2(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; +var __getOwnPropNames$1 = Object.getOwnPropertyNames; +var __commonJS$1 = (cb, mod) => function __require() { + return mod || (0, cb[__getOwnPropNames$1(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; }; -var require_worker_runtime$2 = __commonJS$2({ +var require_worker_runtime$1 = __commonJS$1({ "@remix-pwa/worker-runtime"(exports, module) { module.exports = {}; } }); -var worker_runtime_default$2 = require_worker_runtime$2(); +var worker_runtime_default$1 = require_worker_runtime$1(); const route0 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, - default: worker_runtime_default$2 + default: worker_runtime_default$1 }, Symbol.toStringTag, { value: "Module" })); -const workerAction$2 = async ({ context }) => { - const { fetchFromServer } = context; - console.log("Worker action called"); - try { - const response = await fetchFromServer(); - console.log(Object.fromEntries(response.headers.entries())); - } catch (error) { - console.error(error); - } - return new Response(JSON.stringify({ - message: "Modified action response, Remix Actions are quite out of the picture here" - }), { - headers: { - "Content-Type": "application/json; charset=utf-8" - } - }); +const workerLoader$1 = () => { + console.log("Worker loader called in logout"); + return null; }; const route1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, - workerAction: workerAction$2 + workerLoader: workerLoader$1 }, Symbol.toStringTag, { value: "Module" })); -async function workerLoader$3({ context }) { - const { fetchFromServer } = context; - const message = await Promise.race([ - fetchFromServer().then((response) => response.json()).then(({ message: message2 }) => message2).catch(() => new Promise((resolve) => setTimeout(() => resolve(null), 5e3))), - // utilizing a slower one even when cached - new Promise((resolve) => setTimeout(resolve, 500, "Hello World!\n\nā€¢ This message is sent to you from the client šŸ˜œ (Edited, again ---)!")) - ]); - return new Response( - JSON.stringify({ - message - }), - { - headers: { - "Content-Type": "application/json" - } - } - ); -} +const workerLoader = () => { + console.log("Worker loader called"); + return null; +}; const route2 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ __proto__: null, - workerLoader: workerLoader$3 + workerLoader +}, Symbol.toStringTag, { value: "Module" })); +var __getOwnPropNames = Object.getOwnPropertyNames; +var __commonJS = (cb, mod) => function __require() { + return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; +}; +var require_worker_runtime = __commonJS({ + "@remix-pwa/worker-runtime"(exports, module) { + module.exports = {}; + } +}); +var worker_runtime_default = require_worker_runtime(); +const route3 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ + __proto__: null, + default: worker_runtime_default }, Symbol.toStringTag, { value: "Module" })); +const assets = [ + "/entry.worker.css", + "/entry.worker.js", + "/favicon.ico", + "/manifest.json" +]; +const routes = { + "root": { + id: "root", + parentId: void 0, + path: "", + index: void 0, + caseSensitive: void 0, + hasLoader: false, + hasAction: false, + hasWorkerLoader: false, + hasWorkerAction: false, + module: route0 + }, + "routes/logout": { + id: "routes/logout", + parentId: "root", + path: "logout", + index: void 0, + caseSensitive: void 0, + hasLoader: false, + hasAction: false, + hasWorkerLoader: true, + hasWorkerAction: false, + module: route1 + }, + "routes/_index": { + id: "routes/_index", + parentId: "root", + path: void 0, + index: true, + caseSensitive: void 0, + hasLoader: false, + hasAction: false, + hasWorkerLoader: true, + hasWorkerAction: false, + module: route2 + }, + "routes/test": { + id: "routes/test", + parentId: "root", + path: "test", + index: void 0, + caseSensitive: void 0, + hasLoader: false, + hasAction: false, + hasWorkerLoader: false, + hasWorkerAction: false, + module: route3 + } +}; +const entry = { module: entryWorker }; /** * @remix-run/router v1.18.0 * @@ -9114,233 +9163,6 @@ const router$2 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProp resolveTo, stripBasename }, Symbol.toStringTag, { value: "Module" })); -const workerAction$1 = async ({ request, context }) => { - const formData = await request.formData(); - const { database, fetchFromServer } = context; - try { - fetchFromServer(); - await database.selections.add(Object.fromEntries(formData.entries())); - return redirect$1("/selection"); - } catch (error) { - throw json$1({ message: "Something went wrong", error }, 500); - } -}; -const workerLoader$2 = async ({ context }) => { - try { - const { fetchFromServer, database } = context; - const [serverResult, clientResult] = await Promise.allSettled([ - // NOTE: If the user decides to use the server loader, must use the `context.event.request` object instead of `request`. - // This is because we strip the `_data` and `index` from the request object just to follow what Remix does. - fetchFromServer().then((response) => response.json()).then(({ flights: flights2 }) => flights2), - database.flights.toArray() - ]); - const flights = serverResult.value || clientResult.value; - if (serverResult.value) { - await database.flights.bulkPut( - flights.map((f) => ({ - ...f, - flightNumber: `${f.flightNumber.split("-")[0].trim()} - client` - })) - ); - } - return defer$1({ flights }); - } catch (error) { - console.error(error); - throw json$1({ message: "Something went wrong", error }, 500); - } -}; -const route3 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ - __proto__: null, - workerAction: workerAction$1, - workerLoader: workerLoader$2 -}, Symbol.toStringTag, { value: "Module" })); -async function workerLoader$1({ context }) { - const { database } = context; - const selections = await database.selections.toArray(); - return json$1({ selections }); -} -const route4 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ - __proto__: null, - workerLoader: workerLoader$1 -}, Symbol.toStringTag, { value: "Module" })); -const workerAction = async ({ context }) => { - const { fetchFromServer, event } = context; - try { - await fetchFromServer(); - } catch (error) { - console.error(error); - } - return new Response(JSON.stringify({ - message: "Offline or Online. I shall always respond!" - }), { - headers: { - "Content-Type": "application/json; charset=utf-8" - } - }); -}; -const route5 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ - __proto__: null, - workerAction -}, Symbol.toStringTag, { value: "Module" })); -var __getOwnPropNames$1 = Object.getOwnPropertyNames; -var __commonJS$1 = (cb, mod) => function __require() { - return mod || (0, cb[__getOwnPropNames$1(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; -}; -var require_worker_runtime$1 = __commonJS$1({ - "@remix-pwa/worker-runtime"(exports, module) { - module.exports = {}; - } -}); -var worker_runtime_default$1 = require_worker_runtime$1(); -const route6 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ - __proto__: null, - default: worker_runtime_default$1 -}, Symbol.toStringTag, { value: "Module" })); -var __getOwnPropNames = Object.getOwnPropertyNames; -var __commonJS = (cb, mod) => function __require() { - return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; -}; -var require_worker_runtime = __commonJS({ - "@remix-pwa/worker-runtime"(exports, module) { - module.exports = {}; - } -}); -var worker_runtime_default = require_worker_runtime(); -const route7 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ - __proto__: null, - default: worker_runtime_default -}, Symbol.toStringTag, { value: "Module" })); -async function workerLoader({ context }) { - const { fetchFromServer } = context; - const data = await fetchFromServer().then((response) => response.json()); - console.log(data); - return new Response(JSON.stringify(data), { - headers: { - "Content-Type": "application/json" - } - }); -} -const route8 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({ - __proto__: null, - workerLoader -}, Symbol.toStringTag, { value: "Module" })); -const assets = [ - "/entry.worker.css", - "/entry.worker.js", - "/favicon.ico", - "/manifest.json" -]; -const routes = { - "root": { - id: "root", - parentId: void 0, - path: "", - index: void 0, - caseSensitive: void 0, - hasLoader: false, - hasAction: false, - hasWorkerLoader: false, - hasWorkerAction: false, - module: route0 - }, - "routes/basic-action": { - id: "routes/basic-action", - parentId: "root", - path: "basic-action", - index: void 0, - caseSensitive: void 0, - hasLoader: true, - hasAction: true, - hasWorkerLoader: false, - hasWorkerAction: true, - module: route1 - }, - "routes/basic-loader": { - id: "routes/basic-loader", - parentId: "root", - path: "basic-loader", - index: void 0, - caseSensitive: void 0, - hasLoader: true, - hasAction: false, - hasWorkerLoader: true, - hasWorkerAction: false, - module: route2 - }, - "routes/_app.flights": { - id: "routes/_app.flights", - parentId: "routes/_app", - path: "flights", - index: void 0, - caseSensitive: void 0, - hasLoader: true, - hasAction: true, - hasWorkerLoader: true, - hasWorkerAction: true, - module: route3 - }, - "routes/selection": { - id: "routes/selection", - parentId: "root", - path: "selection", - index: void 0, - caseSensitive: void 0, - hasLoader: true, - hasAction: false, - hasWorkerLoader: true, - hasWorkerAction: false, - module: route4 - }, - "routes/sync-away": { - id: "routes/sync-away", - parentId: "root", - path: "sync-away", - index: void 0, - caseSensitive: void 0, - hasLoader: false, - hasAction: true, - hasWorkerLoader: false, - hasWorkerAction: true, - module: route5 - }, - "routes/_index": { - id: "routes/_index", - parentId: "root", - path: void 0, - index: true, - caseSensitive: void 0, - hasLoader: true, - hasAction: false, - hasWorkerLoader: false, - hasWorkerAction: false, - module: route6 - }, - "routes/push": { - id: "routes/push", - parentId: "root", - path: "push", - index: void 0, - caseSensitive: void 0, - hasLoader: false, - hasAction: false, - hasWorkerLoader: false, - hasWorkerAction: false, - module: route7 - }, - "routes/_app": { - id: "routes/_app", - parentId: "root", - path: void 0, - index: void 0, - caseSensitive: void 0, - hasLoader: true, - hasAction: false, - hasWorkerLoader: true, - hasWorkerAction: false, - module: route8 - } -}; -const entry = { module: entryWorker }; var mode$2 = {}; /** * @remix-run/server-runtime v2.10.3 @@ -9594,13 +9416,21 @@ function createArgumentsFrom({ event, loadContext, path }) { function isMethod(request, methods) { return methods.includes(request.method.toLowerCase()); } -function isActionRequest(request) { +function isLoaderMethod(request) { + return isMethod(request, ["get"]); +} +function isActionMethod(request) { + return isMethod(request, ["post", "delete", "put", "patch", "head"]); +} +function isActionRequest(request, spaMode = false) { const url = new URL(request.url); - return isMethod(request, ["post", "delete", "put", "patch"]) && url.searchParams.get("_data"); + const qualifies = spaMode ? true : url.searchParams.get("_data"); + return isActionMethod(request) && qualifies; } -function isLoaderRequest(request) { +function isLoaderRequest(request, spaMode = false) { const url = new URL(request.url); - return isMethod(request, ["get"]) && url.searchParams.get("_data"); + const qualifies = spaMode ? true : url.searchParams.get("_data"); + return isLoaderMethod(request) && qualifies; } function errorResponseToJson(errorResponse) { return json_1(errorResponse.error || { message: "Unexpected Server Error" }, { @@ -9616,6 +9446,7 @@ function isRemixResponse(response) { } async function handleRequest({ defaultHandler: defaultHandler2, errorHandler, event, loadContext, routes: routes2 }) { var _a; + const isSPAMode = true === "true"; const url = new URL(event.request.url); const routeId = url.searchParams.get("_data"); const route = routeId ? routes2[routeId] : void 0; @@ -9625,7 +9456,7 @@ async function handleRequest({ defaultHandler: defaultHandler2, errorHandler, ev context: loadContext }; try { - if (isLoaderRequest(event.request) && (route == null ? void 0 : route.module.workerLoader)) { + if (isLoaderRequest(event.request, isSPAMode) && (route == null ? void 0 : route.module.workerLoader)) { return await handleLoader({ event, loader: route.module.workerLoader, @@ -9634,7 +9465,7 @@ async function handleRequest({ defaultHandler: defaultHandler2, errorHandler, ev loadContext }).then(responseHandler); } - if (isActionRequest(event.request) && ((_a = route == null ? void 0 : route.module) == null ? void 0 : _a.workerAction)) { + if (isActionRequest(event.request, isSPAMode) && ((_a = route == null ? void 0 : route.module) == null ? void 0 : _a.workerAction)) { return await handleAction({ event, action: route.module.workerAction, diff --git a/playground/spa/entry.client.tsx b/playground/spa/entry.client.tsx new file mode 100644 index 00000000..22cb4908 --- /dev/null +++ b/playground/spa/entry.client.tsx @@ -0,0 +1,20 @@ +/** + * By default, Remix will handle hydrating your app on the client for you. + * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` āœØ + * For more information, see https://remix.run/file-conventions/entry.client + */ + +import { RemixBrowser } from "@remix-run/react"; +// import { spaFetch } from "@remix-pwa/sw"; +import { startTransition } from "react"; +import { hydrateRoot } from "react-dom/client"; + +// window.fetch = spaFetch; + +startTransition(() => { + hydrateRoot( + document, + + ); +}); + diff --git a/playground/spa/entry.worker.ts b/playground/spa/entry.worker.ts new file mode 100644 index 00000000..d4575df7 --- /dev/null +++ b/playground/spa/entry.worker.ts @@ -0,0 +1,108 @@ +/// + +import { EnhancedCache, isDocumentRequest, isLoaderRequest, logger, NavigationHandler } from '@remix-pwa/sw'; +import { PushManager } from '@remix-pwa/push/client'; + +declare let self: ServiceWorkerGlobalScope; + +console.log('Hello from service worker!'); +// @ts-ignore +console.log(process.env.NODE_ENV, process.env.API_URL, miscellaneous); + +const documentCache = new EnhancedCache('document-cache', { + version: 'v1', + strategy: 'NetworkFirst', + strategyOptions: {} +}) + +const assetCache = new EnhancedCache('asset-cache', { + version: 'v1', + strategy: 'CacheFirst', + strategyOptions: {} +}) + +const dataCache = new EnhancedCache('data-cache', { + version: 'v1', + strategy: 'NetworkFirst', + strategyOptions: {} +}) + +/** + * The load context works same as in Remix. The return values of this function will be injected in the worker action/loader. + * @param {FetchEvent} [event] The fetch event request. + * @returns {object} the context object. + */ +export const getLoadContext = () => { + // const stores = createStorageRepository(); + + return { + database: [], + stores: [], + caches: [documentCache, dataCache], + }; +}; + +const isAssetRequest = (request: Request)=> { + const url = new URL(request.url); + + const hasNoParams = url.search === ''; + + return self.__workerManifest.assets.includes(url.pathname) && hasNoParams; +} + +export const defaultFetchHandler = async ({ request, context }: any) => { + const req = context.event.request; + + // console.log(req) + + // The referrer (the URL of the page that made the request) + const referrer = req.referrer; // This is usually the full URL of the document + + // You can also check the 'Referer' header (note the different spelling) + const refererHeader = req.headers.get('referer'); + + if ((req as Request).destination === 'image') { + console.log('Image request', req); + } + + // console.log('Referrer (page making the request):', referrer, req.url, refererHeader, Object.fromEntries(req.headers)); + + if (isAssetRequest(request)) { + return assetCache.handleRequest(request); + } + + if (isDocumentRequest(request)) { + return documentCache.handleRequest(request); + } + + const url = new URL(context.event.request.url); + + // If it is loader request, and there's no worker route API for it, + // we have to run it ourselves + if (isLoaderRequest(request) && self.__workerManifest.routes[url.searchParams.get('_data') ?? ''].hasLoader) { + return dataCache.handleRequest(request); + } + + // logger.log('default handler'); + return context.fetchFromServer(); +} + +self.addEventListener('install', (event: ExtendableEvent) => { + logger.log('installing service worker'); + logger.warn('This is a playground service worker šŸ“¦. It is not intended for production use.'); + event.waitUntil(self.skipWaiting()); +}); + +self.addEventListener('activate', event => { + event.waitUntil(self.clients.claim()); +}); + +const msgHandler = new NavigationHandler({ + cache: documentCache, +}) + +self.addEventListener('message', async event => { + await msgHandler.handleMessage(event); +}) + +new PushManager() diff --git a/playground/spa/root.tsx b/playground/spa/root.tsx new file mode 100644 index 00000000..99a97970 --- /dev/null +++ b/playground/spa/root.tsx @@ -0,0 +1,37 @@ +import { + ManifestLink, + useSWEffect, +} from "@remix-pwa/sw"; +import { + Links, + Meta, + Outlet, + Scripts, + ScrollRestoration, +} from "@remix-run/react"; +import { useEffect } from "react"; + +export default function App() { + useSWEffect() + + useEffect(() => { + if (typeof window !== 'undefined') console.log(window.__remixContext) + }, []) + + return ( + + + + + + + + + + + + + + + ); +} diff --git a/playground/spa/routes/_index.tsx b/playground/spa/routes/_index.tsx new file mode 100644 index 00000000..57e1e03b --- /dev/null +++ b/playground/spa/routes/_index.tsx @@ -0,0 +1,136 @@ +import { usePWAManager } from "@remix-pwa/client"; +import { ClientLoaderFunctionArgs, Form, Link, useFetcher } from "@remix-run/react"; + +export const meta = () => { + return [ + { title: "šŸ“¦ (SPA) Remix PWA Sandbox" }, + { name: "description", content: "Progressive web apps proof of concept" }, + ]; +}; + +const CustomLink = ({ href, children }: any) => { + return ( + {children} {'>>'} + ) +} + +export const workerLoader = () => { + console.log('Worker loader called'); + return null; +} + +export const clientAction = async ({ request, context }: ClientLoaderFunctionArgs) => { + console.log('Client action called'); + console.log(request, context) + + await fetch('https://example.com', { method: 'POST', mode: 'no-cors' }); + + return null; +} + +export default function Index() { + const fetcher = useFetcher(); + const { promptInstall } = usePWAManager(); + + return ( +
+
+

Remix PWA - Worker Actions & Loaders

+ +
+
+ +
+ Get out +
+
+

Basic Worker Loaders

+
+ Remix PWA evolution now includes loaders and actions that run solely on the worker thread. Utilizing esbuild + in its new compiler, Remix PWA now seeks to provide a more robust and performant experience for developers. +
+ Basic loaders route +
+
+

Basic Worker Actions

+
+ Worker actions are much more wicked than their loader counterparts. That's also right, Remix PWA now intercepts +  everything! Including POST, PUT, etc. requests. This means that you can now + intercept all requests and do whatever you want with them. Click the link below to see it in action! +  (got it, eh?) +
+ Basic actions route +
+
+

Pathless routes

+
+ Worker loaders & actions aren't just restricted to your normal routes, they also work in pathless routes! + And yes, they have the same behaviour as Remix normal loaders and actions in that they are nested and exhibit + a waterfall effect. Make sure to 'Log in' else unknown horrors awaits! +
+ Pathless Route +
+
+

Syncing your app šŸ’æ

+
+ Normally when you have no network and something fails, if it isn't something cached like a page or data, you get + an error and that's it. What if I submitted something? Or requested something? The user had to manually retry again. + Good thing we used the word "had", click the link to find out what's changed. +
+ Break the internet +
+
+

Basic Caching

+
+ Remix PWA now allows you to cache directly in your routes. Talk about finetuning your app's performance! + In this demo, we would be caching the response from the server and returning it if it's available. If not, + we would be returning the client's response instead. This is a very powerful feature that allows you to + cache your responses directly in your routes. See you on the other side of the link šŸš€! +
+ Getting started with caching +
+
+

Caching Strategies

+
+ Remix PWA has embraced the strategy approach since remix-pwa@v2 and we haven't let go in the next major + version bump. Bringing back all four strategies (Cache Only, Cache First, Network Only, Network First) as well as a + new one - Stale While Revalidate, this version +
+ Explore caching strategies +
+
+

Caching: A deeper dive šŸ¤æ

+
+ We have so far been using strategy wrappers to interact with the cache, a cache that seems super-charged thanks to its + new capabilities. How about we dive a bit deeper under the hood and tinker with stuffs by ourselves? Remix PWA is + against too much abstraction and we abstract only when deemed necessary, meaning you can say hello + to @remix-pwa/cache! The package that makes all of this possible. +
+ Begin the dive šŸŠā€ā™€ļø +
+
+

Utilities: A lot of them too!

+
+ This update doesn't just bring new caching packages, it also includes the long-awaited +  @remix-pwa/client package that enhances the feel of your app much further. Follow the link + to head into a no-rule zone showcasing some (we can't showcase all!) of the prominent features this update brings. +
+ Begin the dive šŸŠā€ā™€ļø +
+
+
+ ); +} diff --git a/playground/spa/routes/logout.tsx b/playground/spa/routes/logout.tsx new file mode 100644 index 00000000..fb8c0370 --- /dev/null +++ b/playground/spa/routes/logout.tsx @@ -0,0 +1,21 @@ +import { ClientActionFunctionArgs } from "@remix-run/react"; + +export const workerLoader = () => { + console.log('Worker loader called in logout'); + return null; +} + +export const clientAction = async ({ request }: ClientActionFunctionArgs) => { + console.log('clientAction in logout'); + console.log(request) + + await fetch('https://google.com', { method: 'POST', mode: 'no-cors' }); + + return null; +} + +// export default function C( ){ +// return ( +//
Logout?
+// ) +// } \ No newline at end of file diff --git a/playground/spa/routes/test.tsx b/playground/spa/routes/test.tsx new file mode 100644 index 00000000..a577fc1f --- /dev/null +++ b/playground/spa/routes/test.tsx @@ -0,0 +1,11 @@ +export const clientLoader = async () => { + console.log('Client loader called in test'); + await fetch('https://hashnode.com', { method: 'POST', mode: 'no-cors' }); + return null; +} + +export default function Test() { + return ( +
Test
+ ) +} diff --git a/playground/vite.config.ts b/playground/vite.config.ts index b31f7dfa..2d1c7021 100644 --- a/playground/vite.config.ts +++ b/playground/vite.config.ts @@ -6,12 +6,14 @@ import { remixPWA } from '@remix-pwa/dev'; installGlobals(); +const spaMode = true; + export default defineConfig({ plugins: [ remix({ ignoredRouteFiles: ["**/.*"], - appDirectory: './src/app', - // ssr: false, + appDirectory: spaMode ? './spa' : './src/app', + ssr: !spaMode, }), tsconfigPaths(), remixPWA({