Skip to content

Commit

Permalink
feat(visual-editing): add basePath support (#1510)
Browse files Browse the repository at this point in the history
  • Loading branch information
stipsan committed May 21, 2024
1 parent e4b3aa8 commit 9378e61
Show file tree
Hide file tree
Showing 7 changed files with 142 additions and 12 deletions.
2 changes: 1 addition & 1 deletion apps/mvp/app/sanity.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const client = createClient({
resultSourceMap: 'withKeyArraySelector',
stega: {
enabled: true,
studioUrl: '/studio',
studioUrl: `${process.env.NEXT_PUBLIC_TEST_BASE_PATH || ''}/studio`,
// logger: console,
},
})
Expand Down
1 change: 1 addition & 0 deletions apps/mvp/next.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ function requireResolve(id) {

/** @type {import('next').NextConfig} */
const nextConfig = {
basePath: process.env.NEXT_PUBLIC_TEST_BASE_PATH,
experimental: {
taint: true,
},
Expand Down
8 changes: 5 additions & 3 deletions apps/mvp/sanity.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,17 @@ const projectId = process.env.NEXT_PUBLIC_SANITY_PROJECT_ID!
const dataset = process.env.NEXT_PUBLIC_SANITY_DATASET!

const previewMode = {
enable: '/api/draft',
enable: `${process.env.NEXT_PUBLIC_TEST_BASE_PATH || ''}/api/draft`,
} satisfies PreviewUrlResolverOptions['previewMode']

function createConfig(basePath: string, stable: boolean) {
const name = stable ? 'stable' : 'experimental'
const presentationTool = stable ? stablePresentationTool : experimentalPresentationTool
return defineConfig({
name,
basePath: `${basePath}/${name}`,
basePath: `${process.env.NEXT_PUBLIC_TEST_BASE_PATH || ''}${basePath}/${name}`,
// basePath: `${basePath}/${name}`,
// basePath: `${name}`,

projectId,
dataset,
Expand All @@ -36,7 +38,7 @@ function createConfig(basePath: string, stable: boolean) {
assist(),
debugSecrets(),
presentationTool({
previewUrl: {previewMode},
previewUrl: {preview: process.env.NEXT_PUBLIC_TEST_BASE_PATH || '/', previewMode},
}),
structureTool(),
visionTool(),
Expand Down
25 changes: 19 additions & 6 deletions packages/next-sanity/src/visual-editing/client-component/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {usePathname, useRouter, useSearchParams} from 'next/navigation.js'
import {revalidateRootLayout} from 'next-sanity/visual-editing/server-actions'
import {useEffect, useRef, useState} from 'react'

import {addPathPrefix, removePathPrefix} from './utils'

/**
* @public
*/
Expand All @@ -16,10 +18,18 @@ export interface VisualEditingProps extends Omit<VisualEditingOptions, 'history'
* @deprecated The histoy adapter is already implemented
*/
history?: never
/**
* If next.config.ts is configured with a basePath we try to configure it automatically,
* you can disable this by setting basePath to ''.
* @example basePath="/my-custom-base-path"
* @alpha experimental and may change without notice
* @defaultValue process.env.__NEXT_ROUTER_BASEPATH || ''
*/
basePath?: string
}

export default function VisualEditing(props: VisualEditingProps): null {
const {refresh, zIndex} = props
const {refresh, zIndex, basePath = ''} = props

const router = useRouter()
const routerRef = useRef(router)
Expand Down Expand Up @@ -53,11 +63,11 @@ export default function VisualEditing(props: VisualEditingProps): null {
update: (update) => {
switch (update.type) {
case 'push':
return routerRef.current.push(update.url)
return routerRef.current.push(removePathPrefix(update.url, basePath))
case 'pop':
return routerRef.current.back()
case 'replace':
return routerRef.current.replace(update.url)
return routerRef.current.replace(removePathPrefix(update.url, basePath))
default:
throw new Error(`Unknown update type: ${update.type}`)
}
Expand Down Expand Up @@ -96,18 +106,21 @@ export default function VisualEditing(props: VisualEditingProps): null {
}

return () => disable()
}, [refresh, zIndex])
}, [basePath, refresh, zIndex])

const pathname = usePathname()
const searchParams = useSearchParams()
useEffect(() => {
if (navigate) {
navigate({
type: 'push',
url: `${pathname}${searchParams?.size ? `?${searchParams}` : ''}`,
url: addPathPrefix(
`${pathname}${searchParams?.size ? `?${searchParams}` : ''}`,
basePath,
),
})
}
}, [navigate, pathname, searchParams])
}, [basePath, navigate, pathname, searchParams])

return null
}
99 changes: 99 additions & 0 deletions packages/next-sanity/src/visual-editing/client-component/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
/**
* From: https://github.com/vercel/next.js/blob/5469e6427b54ab7e9876d4c85b47f9c3afdc5c1f/packages/next/src/shared/lib/router/utils/path-has-prefix.ts#L10-L17
* Checks if a given path starts with a given prefix. It ensures it matches
* exactly without containing extra chars. e.g. prefix /docs should replace
* for /docs, /docs/, /docs/a but not /docsss
* @param path The path to check.
* @param prefix The prefix to check against.
*/
function pathHasPrefix(path: string, prefix: string): boolean {
if (typeof path !== 'string') {
return false
}

const {pathname} = parsePath(path)
return pathname === prefix || pathname.startsWith(`${prefix}/`)
}

/**
* From: https://github.com/vercel/next.js/blob/5469e6427b54ab7e9876d4c85b47f9c3afdc5c1f/packages/next/src/shared/lib/router/utils/parse-path.ts#L6-L22
* Given a path this function will find the pathname, query and hash and return
* them. This is useful to parse full paths on the client side.
* @param path A path to parse e.g. /foo/bar?id=1#hash
*/
function parsePath(path: string): {
pathname: string
query: string
hash: string
} {
const hashIndex = path.indexOf('#')
const queryIndex = path.indexOf('?')
const hasQuery = queryIndex > -1 && (hashIndex < 0 || queryIndex < hashIndex)

if (hasQuery || hashIndex > -1) {
return {
pathname: path.substring(0, hasQuery ? queryIndex : hashIndex),
query: hasQuery ? path.substring(queryIndex, hashIndex > -1 ? hashIndex : undefined) : '',
hash: hashIndex > -1 ? path.slice(hashIndex) : '',
}
}

return {pathname: path, query: '', hash: ''}
}

/**
* From: https://github.com/vercel/next.js/blob/5469e6427b54ab7e9876d4c85b47f9c3afdc5c1f/packages/next/src/shared/lib/router/utils/add-path-prefix.ts#L3C1-L14C2
* Adds the provided prefix to the given path. It first ensures that the path
* is indeed starting with a slash.
*/
export function addPathPrefix(path: string, prefix?: string): string {
if (!path.startsWith('/') || !prefix) {
return path
}
// If the path is exactly '/' then return just the prefix
if (path === '/' && prefix) {
return prefix
}

const {pathname, query, hash} = parsePath(path)
return `${prefix}${pathname}${query}${hash}`
}

/**
* From: https://github.com/vercel/next.js/blob/5469e6427b54ab7e9876d4c85b47f9c3afdc5c1f/packages/next/src/shared/lib/router/utils/remove-path-prefix.ts#L3-L39
* Given a path and a prefix it will remove the prefix when it exists in the
* given path. It ensures it matches exactly without containing extra chars
* and if the prefix is not there it will be noop.
*
* @param path The path to remove the prefix from.
* @param prefix The prefix to be removed.
*/
export function removePathPrefix(path: string, prefix: string): string {
// If the path doesn't start with the prefix we can return it as is. This
// protects us from situations where the prefix is a substring of the path
// prefix such as:
//
// For prefix: /blog
//
// /blog -> true
// /blog/ -> true
// /blog/1 -> true
// /blogging -> false
// /blogging/ -> false
// /blogging/1 -> false
if (!pathHasPrefix(path, prefix)) {
return path
}

// Remove the prefix from the path via slicing.
const withoutPrefix = path.slice(prefix.length)

// If the path without the prefix starts with a `/` we can return it as is.
if (withoutPrefix.startsWith('/')) {
return withoutPrefix
}

// If the path without the prefix doesn't start with a `/` we need to add it
// back to the path to make sure it's a valid path.
return `/${withoutPrefix}`
}
17 changes: 16 additions & 1 deletion packages/next-sanity/src/visual-editing/index.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable dot-notation */
import type {VisualEditingProps} from 'next-sanity/visual-editing/client-component'
import {lazy, Suspense} from 'react'

Expand All @@ -7,9 +8,23 @@ const VisualEditingComponent = lazy(() => import('next-sanity/visual-editing/cli
* @public
*/
export function VisualEditing(props: VisualEditingProps): React.ReactElement {
let autoBasePath: string | undefined
if (typeof props.basePath !== 'string') {
try {
autoBasePath = process.env['__NEXT_ROUTER_BASEPATH']
if (autoBasePath) {
// eslint-disable-next-line no-console
console.log(
`Detected next basePath as ${JSON.stringify(autoBasePath)} by reading "process.env.__NEXT_ROUTER_BASEPATH". If this is incorrect then you can set it manually with the basePath prop on the <VisualEditing /> component.`,
)
}
} catch (err) {
console.error('Failed detecting basePath', err)
}
}
return (
<Suspense fallback={null}>
<VisualEditingComponent {...props} />
<VisualEditingComponent {...props} basePath={props.basePath ?? autoBasePath} />
</Suspense>
)
}
Expand Down
2 changes: 1 addition & 1 deletion turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"pipeline": {
"build": {
"dotEnv": [".env", ".env.local"],
"env": ["NEXT_PUBLIC_SANITY_*", "NEXT_PUBLIC_VERCEL_ENV"],
"env": ["NEXT_PUBLIC_SANITY_*", "NEXT_PUBLIC_VERCEL_ENV", "NEXT_PUBLIC_TEST_BASE_PATH"],
"outputs": [".next/**", "!.next/cache/**", "out/**"],
"dependsOn": ["^build"]
},
Expand Down

0 comments on commit 9378e61

Please sign in to comment.