Skip to content

Commit

Permalink
feat(workspaces): add more validation of basePath and names
Browse files Browse the repository at this point in the history
  • Loading branch information
rexxars committed Aug 10, 2022
1 parent 951dbc6 commit 82df86a
Show file tree
Hide file tree
Showing 8 changed files with 448 additions and 169 deletions.
@@ -1,92 +1,5 @@
import assert from 'assert'
import {matchWorkspace, validateWorkspaceBasePaths} from '../matchWorkspace'

describe('validateWorkspaceBasePaths', () => {
it('allows empty basePaths', () => {
validateWorkspaceBasePaths({
workspaces: [{name: 'foo', basePath: '/'}],
})

validateWorkspaceBasePaths({
workspaces: [{name: 'foo', basePath: ''}],
})

validateWorkspaceBasePaths({
workspaces: [{name: 'foo', basePath: undefined}],
})
})

it('throws if a workspace has invalid characters', () => {
expect(() => {
validateWorkspaceBasePaths({
workspaces: [{name: 'foo', basePath: '\tinvalid.characters%everywhere '}],
})
}).toThrowErrorMatchingInlineSnapshot(
`"All workspace \`basePath\`s must start with a leading \`/\`, consist of only URL safe characters, and cannot end with a trailing \`/\`. Workspace \`foo\`'s basePath is \` invalid.characters%everywhere \`"`
)
})

it("throws if a workspace doesn't start with a leading `/`", () => {
expect(() => {
validateWorkspaceBasePaths({
workspaces: [{name: 'foo', basePath: 'no-leading-slash'}],
})
}).toThrowErrorMatchingInlineSnapshot(
`"All workspace \`basePath\`s must start with a leading \`/\`, consist of only URL safe characters, and cannot end with a trailing \`/\`. Workspace \`foo\`'s basePath is \`no-leading-slash\`"`
)
})

it('throws if a workspace has a trailing `/`', () => {
expect(() => {
validateWorkspaceBasePaths({
workspaces: [{name: 'foo', basePath: '/has-trailing-slash/'}],
})
}).toThrowErrorMatchingInlineSnapshot(
`"All workspace \`basePath\`s must start with a leading \`/\`, consist of only URL safe characters, and cannot end with a trailing \`/\`. Workspace \`foo\`'s basePath is \`/has-trailing-slash/\`"`
)
})

it('allows base paths with a leading `/`, URL safe characters, and no trailing slash', () => {
validateWorkspaceBasePaths({
workspaces: [{name: 'foo', basePath: '/valid'}],
})

validateWorkspaceBasePaths({
workspaces: [{name: 'foo', basePath: '/also/valid'}],
})

validateWorkspaceBasePaths({
workspaces: [{name: 'foo', basePath: '/still-valid'}],
})
})

it('throws if workspace base paths have differing segment counts', () => {
expect(() => {
validateWorkspaceBasePaths({
workspaces: [
{name: 'twoSegments', basePath: '/one/two'},
{name: 'threeSegments', basePath: '/one/two/three'},
],
})
}).toThrowErrorMatchingInlineSnapshot(
`"All workspace \`basePath\`s must have the same amount of segments. Workspace \`twoSegments\` had 2 segments \`/one/two\` but workspace \`threeSegments\` had 3 segments \`/one/two/three\`"`
)
})

it('throws if workspaces have identical base paths', () => {
expect(() => {
validateWorkspaceBasePaths({
workspaces: [
{name: 'foo', basePath: '/one/two'},
{name: 'bar', basePath: '/foo/bar'},
{name: 'fooAgain', basePath: '/OnE/TwO'},
],
})
}).toThrowErrorMatchingInlineSnapshot(
`"\`basePath\`s must be unique. Workspaces \`foo\` and \`fooAgain\` both have the \`basePath\` \`/one/two\`"`
)
})
})
import {matchWorkspace} from '../matchWorkspace'

describe('matchWorkspace', () => {
it('returns a match if the incoming `pathname` matches a workspace `basePath`', () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/sanity/src/studio/activeWorkspaceMatcher/index.ts
@@ -1,4 +1,4 @@
export {ActiveWorkspaceMatcherContext} from './ActiveWorkspaceMatcherContext'
export {ActiveWorkspaceMatcher} from './ActiveWorkspaceMatcher'
export {useActiveWorkspace} from './useActiveWorkspace'
export {matchWorkspace, validateWorkspaceBasePaths} from './matchWorkspace'
export {matchWorkspace} from './matchWorkspace'
@@ -1,83 +1,5 @@
import {escapeRegExp} from 'lodash'

interface WorkspaceLike {
name: string
basePath?: string
}

interface ValidateWorkspaceBasePathsOptions {
workspaces: WorkspaceLike[]
}

/**
* Throws if the base paths in the given workspaces cannot be distinguished from
* each other.
*/
export function validateWorkspaceBasePaths({workspaces}: ValidateWorkspaceBasePathsOptions): void {
if (!workspaces.length) {
throw new Error('At least one workspace is required.')
}

const normalized = workspaces.map(({name, basePath}) => ({name, basePath: basePath || '/'}))

// check for a leading `/`, no trailing slash, and no invalid characters
for (const workspace of normalized) {
// by default, an empty basePath should be converted into a single `/` and
// this case is fine
if (workspace.basePath === '/') continue

if (!/^\/[a-z0-9/_-]*[a-z0-9_-]+$/i.test(workspace.basePath)) {
throw new Error(
`All workspace \`basePath\`s must start with a leading \`/\`, ` +
`consist of only URL safe characters, ` +
`and cannot end with a trailing \`/\`. ` +
`Workspace \`${workspace.name}\`'s basePath is \`${workspace.basePath}\``
)
}
}

const [firstWorkspace, ...restOfWorkspaces] = normalized
const firstWorkspaceSegmentCount = firstWorkspace.basePath
// remove starting slash before splitting
.substring(1)
.split('/').length

for (const workspace of restOfWorkspaces) {
const workspaceSegmentCount = workspace.basePath
// remove starting slash before splitting
.substring(1)
.split('/').length

if (firstWorkspaceSegmentCount !== workspaceSegmentCount) {
throw new Error(
`All workspace \`basePath\`s must have the same amount of segments. Workspace \`${
firstWorkspace.name
}\` had ${firstWorkspaceSegmentCount} segment${
firstWorkspaceSegmentCount === 1 ? '' : 's'
} \`${firstWorkspace.basePath}\` but workspace \`${
workspace.name
}\` had ${workspaceSegmentCount} segment${workspaceSegmentCount === 1 ? '' : 's'} \`${
workspace.basePath
}\``
)
}
}

const basePaths = new Map<string, string>()
for (const workspace of normalized) {
const basePath = workspace.basePath.toLowerCase()

const existingWorkspace = basePaths.get(basePath)
if (existingWorkspace) {
throw new Error(
`\`basePath\`s must be unique. Workspaces \`${existingWorkspace}\` and ` +
`\`${workspace.name}\` both have the \`basePath\` \`${basePath}\``
)
}

basePaths.set(basePath, workspace.name)
}
}
import {WorkspaceLike} from '../workspaces'

interface MatchWorkspaceOptions<TWorkspace extends WorkspaceLike> {
workspaces: TWorkspace[]
Expand All @@ -104,7 +26,6 @@ export function matchWorkspace<TWorkspace extends WorkspaceLike>({
name: workspace.name,
basePath: workspace.basePath || '/',
}))
validateWorkspaceBasePaths({workspaces: normalized})

const [firstWorkspace] = normalized
for (const {workspace, basePath} of normalized) {
Expand Down
22 changes: 22 additions & 0 deletions packages/sanity/src/studio/workspaces/WorkspaceValidationError.ts
@@ -0,0 +1,22 @@
import type {WorkspaceLike} from './types'
import {getWorkspaceIdentifier} from './validateWorkspaces'

export interface WorkspaceValidationErrorOptions {
workspace: WorkspaceLike
index: number
}

/**
* Thrown on workspace validation errors. Includes an identifier that is either the name of
* the workspace, or in the case of a missing or invalid name, an index and a potential title
*/
export class WorkspaceValidationError extends Error {
index?: number
identifier?: string

constructor(message: string, options?: WorkspaceValidationErrorOptions) {
super(message)
this.index = options?.index
this.identifier = options?.workspace && getWorkspaceIdentifier(options.workspace, options.index)
}
}

0 comments on commit 82df86a

Please sign in to comment.