From 85fe798a572eb4f355eed804e41b8d3e64dcde43 Mon Sep 17 00:00:00 2001 From: sigma-andex <77549848+sigma-andex@users.noreply.github.com> Date: Mon, 16 Jan 2023 09:23:40 +0000 Subject: [PATCH 1/3] Add slime additions --- spago.dhall | 12 ++++ src/Next/Document.js | 5 ++ src/Next/Document.purs | 7 ++ src/Next/GetServerSideProps.purs | 81 +++++++++++++++++++++++ src/Next/GetStaticProps.purs | 78 +++++++++++++++++++++++ src/Next/Image.js | 3 + src/Next/Image.purs | 26 ++++++++ src/Next/Router.js | 106 ++++++++++++++++++++++++++++--- src/Next/Router.purs | 104 +++++++++++++++++++++++------- src/Next/Router/PushOptions.purs | 19 ++++++ src/Next/SWR.purs | 15 ----- src/Next/SerialisedProps.purs | 33 ++++++++++ 12 files changed, 440 insertions(+), 49 deletions(-) create mode 100644 src/Next/GetServerSideProps.purs create mode 100644 src/Next/GetStaticProps.purs create mode 100644 src/Next/Image.js create mode 100644 src/Next/Image.purs create mode 100644 src/Next/Router/PushOptions.purs create mode 100644 src/Next/SerialisedProps.purs diff --git a/spago.dhall b/spago.dhall index a1089d4..246d6fb 100644 --- a/spago.dhall +++ b/spago.dhall @@ -15,13 +15,25 @@ to generate this file without the comments in this block. [ "aff" , "aff-promise" , "console" + , "datetime" , "effect" + , "either" + , "foldable-traversable" + , "foreign" + , "foreign-object" , "functions" + , "identity" , "maybe" , "nullable" + , "options" + , "partial" , "prelude" , "react-basic" , "react-basic-hooks" + , "transformers" + , "typelevel-prelude" + , "unsafe-coerce" + , "yoga-json" ] , packages = ./packages.dhall , sources = [ "src/**/*.purs", "test/**/*.purs" ] diff --git a/src/Next/Document.js b/src/Next/Document.js index cf61ebe..b028691 100644 --- a/src/Next/Document.js +++ b/src/Next/Document.js @@ -3,3 +3,8 @@ export var _html = document.Html; export var _head = document.Head; export var _main = document.Main; export var _nextScript = document.NextScript; + +export const unsafeDocument = component => () => { + component.getInitialProps = nextDoc.getInitialProps + return component +} diff --git a/src/Next/Document.purs b/src/Next/Document.purs index 2487092..0b8fe8b 100644 --- a/src/Next/Document.purs +++ b/src/Next/Document.purs @@ -1,5 +1,6 @@ module Next.Document where +import Effect (Effect) import Prim.Row (class Union) import React.Basic.Hooks (JSX, ReactComponent, element) @@ -59,3 +60,9 @@ nextScript nextScript attrs = element _nextScript attrs foreign import _nextScript :: forall attrs. ReactComponent attrs + +-- | Document +foreign import unsafeDocument + ∷ ∀ props + . ReactComponent props + → Effect (ReactComponent props) diff --git a/src/Next/GetServerSideProps.purs b/src/Next/GetServerSideProps.purs new file mode 100644 index 0000000..dc88d6b --- /dev/null +++ b/src/Next/GetServerSideProps.purs @@ -0,0 +1,81 @@ +module Next.GetServerSideProps where + +import Prelude hiding (top) + +import Control.Promise (Promise) +import Control.Promise as Promise +import Data.Either (Either(..)) +import Data.Maybe (Maybe) +import Data.Semigroup.Foldable (intercalateMap) +import Effect.Aff (Aff) +import Effect.Uncurried (EffectFn1, mkEffectFn1) +import Foreign (Foreign, renderForeignError) +import Foreign.Object (Object) +import Next.SerialisedProps (SerialisedProps) +import Partial.Unsafe (unsafeCrashWith) +import Prim.Row (class Union) +import Unsafe.Coerce (unsafeCoerce) +import Yoga.JSON (class ReadForeign, class WriteForeign, read, write, writeJSON) + +type ServerSidePropsContext params = + { params ∷ Maybe { | params } + , req ∷ Foreign -- IncomingMessage + , res ∷ Foreign -- HTTP response object + , query ∷ Object String + , preview ∷ Maybe Boolean + , resolvedUrl ∷ String + , locale ∷ Maybe Locale + , locales ∷ Maybe (Array Locale) + , defaultLocale ∷ Maybe Locale + } + +-- [TODO]: Move to a separate file +newtype Locale = Locale String + +derive newtype instance ReadForeign Locale +derive newtype instance WriteForeign Locale + +type ServerSideProps props = + ( props ∷ props + , redirect ∷ { destination ∷ String, permanent ∷ Boolean } + , notFound ∷ Boolean + ) + +foreign import data GetServerSideProps ∷ Type → Type → Type + +toGetServerSideProps + ∷ ∀ partialProps componentProps params + . (EffectFn1 Foreign (Promise partialProps)) + → GetServerSideProps params componentProps +toGetServerSideProps = unsafeCoerce + +decodeContextOrCrash ∷ ∀ a. ReadForeign a ⇒ Foreign → a +decodeContextOrCrash fgn = + case read fgn of + Right value → value + Left errors → + unsafeCrashWith + $ "Invalid context received in getServerSideProps:\n" + <> intercalateMap "\n" renderForeignError errors + +mkGetServerSideProps + ∷ ∀ props p p_ params + . Union p p_ (ServerSideProps props) + ⇒ ReadForeign { | params } + ⇒ ReadForeign { | p } + ⇒ WriteForeign props + ⇒ WriteForeign { props ∷ { serialisedProps ∷ String } | p } + ⇒ (ServerSidePropsContext params → Aff { props ∷ props | p }) + → GetServerSideProps { | params } (SerialisedProps props) +mkGetServerSideProps constructProps = + toGetServerSideProps + $ mkEffectFn1 + ( Promise.fromAff + <<< (map write) + <<< + ( map \x → + (x { props = { serialisedProps: writeJSON x.props } ∷ SerialisedProps props }) + ) + <<< constructProps + <<< decodeContextOrCrash + ) diff --git a/src/Next/GetStaticProps.purs b/src/Next/GetStaticProps.purs new file mode 100644 index 0000000..6c0c6c6 --- /dev/null +++ b/src/Next/GetStaticProps.purs @@ -0,0 +1,78 @@ +module Next.GetStaticProps where + +import Prelude hiding (top) + +import Control.Promise (Promise) +import Control.Promise as Promise +import Data.Either (Either(..)) +import Data.Maybe (Maybe) +import Data.Semigroup.Foldable (intercalateMap) +import Data.Time.Duration (Seconds) +import Effect.Aff (Aff) +import Effect.Uncurried (EffectFn1, mkEffectFn1) +import Foreign (Foreign, renderForeignError) +import Next.SerialisedProps (SerialisedProps) +import Partial.Unsafe (unsafeCrashWith) +import Prim.Row (class Union) +import Unsafe.Coerce (unsafeCoerce) +import Yoga.JSON (class ReadForeign, class WriteForeign, read, write, writeJSON) + +type StaticPropsContext params props = + { params ∷ Maybe { | params } + , preview ∷ Maybe Boolean + , previewData ∷ Maybe { | props } + , locale ∷ Maybe Locale + , locales ∷ Maybe (Array Locale) + , defaultLocale ∷ Maybe Locale + } + +-- [TODO]: Move to a separate file +newtype Locale = Locale String + +derive newtype instance ReadForeign Locale +derive newtype instance WriteForeign Locale + +type StaticProps props = + ( props ∷ props + , revalidate ∷ Seconds + , notFound ∷ Boolean + ) + +foreign import data GetStaticProps ∷ Type → Type → Type + +toGetStaticProps + ∷ ∀ partialProps componentProps params + . (EffectFn1 Foreign (Promise partialProps)) + → GetStaticProps params componentProps +toGetStaticProps = unsafeCoerce + +decodeContextOrCrash ∷ ∀ a. ReadForeign a ⇒ Foreign → a +decodeContextOrCrash fgn = + case read fgn of + Right value → value + Left errors → + unsafeCrashWith + $ "Invalid context received in getStaticProps:\n" + <> intercalateMap "\n" renderForeignError errors + +mkGetStaticProps + ∷ ∀ props p p_ params + . Union p p_ (StaticProps props) + ⇒ ReadForeign { | params } + ⇒ ReadForeign { | p } + ⇒ WriteForeign props + ⇒ WriteForeign { props ∷ { serialisedProps ∷ String } | p } + ⇒ (StaticPropsContext params p → Aff { props ∷ props | p }) + → GetStaticProps { | params } (SerialisedProps props) +mkGetStaticProps constructProps = + toGetStaticProps + $ mkEffectFn1 + ( Promise.fromAff + <<< (map write) + <<< + ( map \x → + (x { props = { serialisedProps: writeJSON x.props } ∷ SerialisedProps props }) + ) + <<< constructProps + <<< decodeContextOrCrash + ) diff --git a/src/Next/Image.js b/src/Next/Image.js new file mode 100644 index 0000000..2f4547e --- /dev/null +++ b/src/Next/Image.js @@ -0,0 +1,3 @@ +import image from "next/image.js" + +export const imageImpl = image; diff --git a/src/Next/Image.purs b/src/Next/Image.purs new file mode 100644 index 0000000..fb28b8a --- /dev/null +++ b/src/Next/Image.purs @@ -0,0 +1,26 @@ +module Next.Image where + +import Prim.Row (class Union) +import React.Basic.Hooks (ReactComponent) + +foreign import data StaticallyImportedImage ∷ Type + +-- | Components +type Props_static_image = + ( src ∷ StaticallyImportedImage + , alt ∷ String + , layout ∷ String + , objectFit ∷ String + , objectPosition ∷ String + , quality ∷ Int + , placeholder ∷ String + , className ∷ String + ) + +staticImage + ∷ ∀ attrs attrs_ + . Union attrs attrs_ Props_static_image + ⇒ ReactComponent { src ∷ StaticallyImportedImage | attrs } +staticImage = imageImpl + +foreign import imageImpl ∷ ∀ attrs. ReactComponent attrs diff --git a/src/Next/Router.js b/src/Next/Router.js index 21a5be8..3d830d2 100644 --- a/src/Next/Router.js +++ b/src/Next/Router.js @@ -1,27 +1,113 @@ -export { useRouter as useRouter_ } from "next/router"; -import router from "next/router"; +import nextRouter, { useRouter } from "next/router.js"; -export function _on(event) { +export const onImpl = function (event) { return function (cb) { return function () { - router.events.on(event, cb); + nextRouter.events.on(event, cb); }; }; -} +}; -export function _off(event) { +export const offImpl = function (event) { return function (cb) { return function () { - router.events.off(event, cb); + nextRouter.events.off(event, cb); }; }; +}; + +export const useRouterImpl = () => useRouter(); + +export const queryImpl = (nr) => nr.query; + +export function pathnameImpl(nr) { + return () => nr.pathname; +} + +export const asPathImpl = (nr) => nr.asPath; + +export const pushImpl = (path) => (as) => (options) => (routerInstance) => () => + routerInstance.push(path, as, options); + +export function pushImplNoAs(path) { + return (options) => (routerInstance) => () => { + console.log("pushImplNoAs", options); + routerInstance.push(path, undefined, options); + }; } -export const query = (router) => router.query; +export const historyPushState = (href) => (as) => () => { + window.history.pushState( + _objectSpread( + _objectSpread({}, window.history.state), + {}, + { + as: as, + url: href, + } + ), + "", + href + ); +}; + +// Babel stuff +function ownKeys(object, enumerableOnly) { + var keys = Object.keys(object); + if (Object.getOwnPropertySymbols) { + var symbols = Object.getOwnPropertySymbols(object); + if (enumerableOnly) { + symbols = symbols.filter(function (sym) { + return Object.getOwnPropertyDescriptor(object, sym).enumerable; + }); + } + keys.push.apply(keys, symbols); + } + return keys; +} -export const push = (router) => (route) => () => router.push(route); +function _objectSpread(target) { + for (var i = 1; i < arguments.length; i++) { + var source = arguments[i] != null ? arguments[i] : {}; + if (i % 2) { + ownKeys(Object(source), true).forEach(function (key) { + _defineProperty(target, key, source[key]); + }); + } else if (Object.getOwnPropertyDescriptors) { + Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); + } else { + ownKeys(Object(source)).forEach(function (key) { + Object.defineProperty( + target, + key, + Object.getOwnPropertyDescriptor(source, key) + ); + }); + } + } + return target; +} + +function _defineProperty(obj, key, value) { + if (key in obj) { + Object.defineProperty(obj, key, { + value: value, + enumerable: true, + configurable: true, + writable: true, + }); + } else { + obj[key] = value; + } + return obj; +} -export const route = (router) => router.route; +export const mkOnRouteChangeStartHandler = (fn) => () => (url, opts) => + fn(url)(opts)(); +export const addOnRouteChangeStartHandler = (handler) => (router) => () => { + router.events.on("routeChangeStart", handler); + return () => router.events.off("routeChangeStart", handler); +}; export const replaceImpl = (router) => (route) => (as) => (options) => () => router.replace(route, as, options); diff --git a/src/Next/Router.purs b/src/Next/Router.purs index 27e2612..fa5a938 100644 --- a/src/Next/Router.purs +++ b/src/Next/Router.purs @@ -1,4 +1,23 @@ -module Next.Router where +module Next.Router + ( NextRouter + , RouteChangeStartHandler + , UseRouter + , addOnRouteChangeStartHandler + , asPath + , event + , historyPushState + , mkOnRouteChangeStartHandler + , onRouteChangeStart + , pathname + , push + , pushAs + , pushAs_ + , push_ + , replace + , routeChangeComplete + , routeChangeError + , useRouter + ) where import Prelude @@ -7,45 +26,82 @@ import Control.Promise as Promise import Data.Maybe (Maybe) import Data.Nullable (Nullable) import Data.Nullable as Nullable +import Data.Options (Options, options) import Effect (Effect) import Effect.Aff (Aff) -import Effect.Uncurried (mkEffectFn1) +import Effect.Uncurried (EffectFn1, mkEffectFn1, runEffectFn1) +import Foreign (Foreign) +import Next.Router.PushOptions (PushOptions) import React.Basic.Hooks (Hook, unsafeHook) -foreign import data Router :: Type +foreign import onImpl ∷ ∀ a. String → a → Effect Unit -foreign import useRouter_ :: Effect Router +foreign import offImpl ∷ ∀ a. String → a → Effect Unit -useRouter :: Hook (UseRouter) Router -useRouter = unsafeHook useRouter_ +event ∷ ∀ a. String → a → Effect (Effect Unit) +event name cb = onImpl name cb $> offImpl name cb -foreign import data UseRouter :: Type -> Type +onRouteChangeStart ∷ (String → Effect Unit) → Effect (Effect Unit) +onRouteChangeStart = event "routeChangeStart" <<< mkEffectFn1 -foreign import query :: forall q. Router -> q +routeChangeComplete ∷ (String → Effect Unit) → Effect (Effect Unit) +routeChangeComplete = event "routeChangeComplete" <<< mkEffectFn1 -foreign import push :: Router -> String -> Effect Unit +routeChangeError + ∷ ∀ r. ({ cancelled ∷ Boolean | r } → Effect Unit) → Effect (Effect Unit) +routeChangeError = event "routeChangeError" <<< mkEffectFn1 -type RoutingOptions = { shallow :: Boolean } +foreign import data NextRouter ∷ Type -foreign import replaceImpl :: Router -> String -> Nullable String -> Nullable RoutingOptions -> Effect (Promise Boolean) +foreign import useRouterImpl ∷ Effect NextRouter +foreign import queryImpl ∷ NextRouter → Foreign -replace :: Router -> String -> Maybe String -> Maybe RoutingOptions -> Aff Boolean -replace router url asUrl options = replaceImpl router url (Nullable.toNullable asUrl) (Nullable.toNullable options) # Promise.toAffE +foreign import data UseRouter ∷ Type → Type -foreign import route :: Router -> String +useRouter ∷ Hook UseRouter NextRouter +useRouter = unsafeHook useRouterImpl -foreign import _on :: forall a. String -> a -> Effect Unit +foreign import pathnameImpl ∷ NextRouter → Effect String -foreign import _off :: forall a. String -> a -> Effect Unit +pathname :: NextRouter -> Effect String +pathname = pathnameImpl -event :: forall a. String -> a -> Effect (Effect Unit) -event name cb = _on name cb $> _off name cb +foreign import pushImpl + ∷ String → String → Options PushOptions → NextRouter → Effect Unit -onRouteChangeStart :: (String -> Effect Unit) -> Effect (Effect Unit) -onRouteChangeStart = event "routeChangeStart" <<< mkEffectFn1 +foreign import pushImplNoAs + ∷ String → Foreign → NextRouter → Effect Unit -routeChangeComplete :: (String -> Effect Unit) -> Effect (Effect Unit) -routeChangeComplete = event "routeChangeComplete" <<< mkEffectFn1 +push_ ∷ String → NextRouter → Effect Unit +push_ href = push href mempty -routeChangeError :: forall r. ({ cancelled :: Boolean | r } -> Effect Unit) -> Effect (Effect Unit) -routeChangeError = event "routeChangeError" <<< mkEffectFn1 +push ∷ String → Options PushOptions → NextRouter → Effect Unit +push href = options >>> pushImplNoAs href + +pushAs_ ∷ String → String → NextRouter → Effect Unit +pushAs_ href as = pushAs href as (mempty) + +pushAs ∷ String → String → Options PushOptions → NextRouter → Effect Unit +pushAs href as = pushImpl href as + +foreign import historyPushState ∷ String → String → Effect Unit + +foreign import asPathImpl ∷ EffectFn1 NextRouter String + +asPath ∷ NextRouter → Effect String +asPath = runEffectFn1 asPathImpl + +foreign import data RouteChangeStartHandler ∷ Type + +foreign import mkOnRouteChangeStartHandler + ∷ (String → { shallow ∷ Boolean } → Effect Unit) + → Effect RouteChangeStartHandler + +-- | Returns the unregister function for easy use in hooks +foreign import addOnRouteChangeStartHandler + ∷ RouteChangeStartHandler → NextRouter → Effect (Effect Unit) + +foreign import replaceImpl :: NextRouter -> String -> Nullable String -> Options PushOptions -> Effect (Promise Boolean) + +replace :: NextRouter -> String -> Maybe String -> Options PushOptions -> Aff Boolean +replace router url asUrl options = replaceImpl router url (Nullable.toNullable asUrl) options # Promise.toAffE diff --git a/src/Next/Router/PushOptions.purs b/src/Next/Router/PushOptions.purs new file mode 100644 index 0000000..f938c0b --- /dev/null +++ b/src/Next/Router/PushOptions.purs @@ -0,0 +1,19 @@ +module Next.Router.PushOptions + ( PushOptions + , locale + , scroll + , shallow + ) where + +import Data.Options (Option, opt) + +foreign import data PushOptions ∷ Type + +shallow ∷ Option PushOptions Boolean +shallow = opt "shallow" + +locale ∷ Option PushOptions String +locale = opt "locale" + +scroll ∷ Option PushOptions String +scroll = opt "scroll" diff --git a/src/Next/SWR.purs b/src/Next/SWR.purs index e043028..749cceb 100644 --- a/src/Next/SWR.purs +++ b/src/Next/SWR.purs @@ -11,21 +11,6 @@ import React.Basic.Hooks (Hook, ReactComponent, unsafeHook) type SWR d err = { "data" :: d, error :: err } --- const fetcher = (...args) => fetch(...args).then((res) => res.json()) - --- function Profile() { --- const { data, error } = useSWR('/api/profile-data', fetcher) - --- if (error) return
Failed to load
--- if (!data) return
Loading...
- --- return ( ---
---

{data.name}

---

{data.bio}

---
--- ) - foreign import useSWR_ :: forall d err . EffectFn2 diff --git a/src/Next/SerialisedProps.purs b/src/Next/SerialisedProps.purs new file mode 100644 index 0000000..4cabd1c --- /dev/null +++ b/src/Next/SerialisedProps.purs @@ -0,0 +1,33 @@ +module Next.SerialisedProps where + +import Prelude + +import Control.Monad.Except (runExcept) + +import Data.Either (Either(..)) +import Data.Semigroup.Foldable (intercalateMap) +import Foreign (renderForeignError, unsafeToForeign) +import Foreign.Index (readProp) +import Partial.Unsafe (unsafeCrashWith) +import Yoga.JSON (class ReadForeign, E, readJSON) +import Yoga.JSON as JSON + +type SerialisedProps ∷ ∀ k. k → Type +type SerialisedProps a = + { serialisedProps ∷ String + } + +deserialiseProps ∷ ∀ a. ReadForeign a ⇒ SerialisedProps a → E a +deserialiseProps sp = runExcept do + value <- readProp "serialisedProps" (unsafeToForeign sp) + str <- JSON.read' value + JSON.readJSON' str + +unsafeDeserialiseProps ∷ ∀ a. ReadForeign a ⇒ SerialisedProps a → a +unsafeDeserialiseProps props = do + case readJSON props.serialisedProps of + Right value → value + Left errors → + unsafeCrashWith + $ "Invalid serialisedProps getServerSideProps:\n" + <> intercalateMap "\n" renderForeignError errors From 4cf0baec9eac2aba4ed35300a372b027e85e2d9e Mon Sep 17 00:00:00 2001 From: sigma-andex <77549848+sigma-andex@users.noreply.github.com> Date: Mon, 16 Jan 2023 09:24:55 +0000 Subject: [PATCH 2/3] Remove log --- src/Next/Router.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Next/Router.js b/src/Next/Router.js index 3d830d2..5eda163 100644 --- a/src/Next/Router.js +++ b/src/Next/Router.js @@ -31,7 +31,6 @@ export const pushImpl = (path) => (as) => (options) => (routerInstance) => () => export function pushImplNoAs(path) { return (options) => (routerInstance) => () => { - console.log("pushImplNoAs", options); routerInstance.push(path, undefined, options); }; } From 4085d8d2befb5e4ee7365940e559d746585f0f1e Mon Sep 17 00:00:00 2001 From: sigma-andex <77549848+sigma-andex@users.noreply.github.com> Date: Mon, 16 Jan 2023 09:27:07 +0000 Subject: [PATCH 3/3] add query method --- src/Next/Router.purs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Next/Router.purs b/src/Next/Router.purs index fa5a938..9d7778f 100644 --- a/src/Next/Router.purs +++ b/src/Next/Router.purs @@ -13,11 +13,13 @@ module Next.Router , pushAs , pushAs_ , push_ + , query , replace , routeChangeComplete , routeChangeError , useRouter - ) where + ) + where import Prelude @@ -56,6 +58,9 @@ foreign import data NextRouter ∷ Type foreign import useRouterImpl ∷ Effect NextRouter foreign import queryImpl ∷ NextRouter → Foreign +query ∷ NextRouter → Foreign +query = queryImpl + foreign import data UseRouter ∷ Type → Type useRouter ∷ Hook UseRouter NextRouter