diff --git a/packages/docusaurus-plugin-content-blog/src/__tests__/options.test.ts b/packages/docusaurus-plugin-content-blog/src/__tests__/options.test.ts index 92eae01ae5..bb0f53c5f3 100644 --- a/packages/docusaurus-plugin-content-blog/src/__tests__/options.test.ts +++ b/packages/docusaurus-plugin-content-blog/src/__tests__/options.test.ts @@ -40,7 +40,7 @@ describe('validateOptions', () => { ...defaultOptions, feedOptions: {type: 'rss' as const, title: 'myTitle'}, path: 'not_blog', - routeBasePath: 'myBlog', + routeBasePath: '/myBlog', postsPerPage: 5, include: ['api/*', 'docs/*'], }; @@ -53,7 +53,7 @@ describe('validateOptions', () => { it('accepts valid user options', () => { const userOptions: Options = { ...defaultOptions, - routeBasePath: 'myBlog', + routeBasePath: '/myBlog', beforeDefaultRemarkPlugins: [], beforeDefaultRehypePlugins: [markdownPluginsFunctionStub], remarkPlugins: [[markdownPluginsFunctionStub, {option1: '42'}]], diff --git a/packages/docusaurus-plugin-content-blog/src/options.ts b/packages/docusaurus-plugin-content-blog/src/options.ts index 99c4067a9a..804169f733 100644 --- a/packages/docusaurus-plugin-content-blog/src/options.ts +++ b/packages/docusaurus-plugin-content-blog/src/options.ts @@ -10,6 +10,7 @@ import { RemarkPluginsSchema, RehypePluginsSchema, AdmonitionsSchema, + RouteBasePathSchema, URISchema, } from '@docusaurus/utils-validation'; import {GlobExcludeDefault} from '@docusaurus/utils'; @@ -56,10 +57,7 @@ const PluginOptionSchema = Joi.object({ archiveBasePath: Joi.string() .default(DEFAULT_OPTIONS.archiveBasePath) .allow(null), - routeBasePath: Joi.string() - // '' not allowed, see https://github.com/facebook/docusaurus/issues/3374 - // .allow('') - .default(DEFAULT_OPTIONS.routeBasePath), + routeBasePath: RouteBasePathSchema.default(DEFAULT_OPTIONS.routeBasePath), tagsBasePath: Joi.string().default(DEFAULT_OPTIONS.tagsBasePath), include: Joi.array().items(Joi.string()).default(DEFAULT_OPTIONS.include), exclude: Joi.array().items(Joi.string()).default(DEFAULT_OPTIONS.exclude), diff --git a/packages/docusaurus-plugin-content-docs/src/__tests__/options.test.ts b/packages/docusaurus-plugin-content-docs/src/__tests__/options.test.ts index 47535f746c..260eaccb22 100644 --- a/packages/docusaurus-plugin-content-docs/src/__tests__/options.test.ts +++ b/packages/docusaurus-plugin-content-docs/src/__tests__/options.test.ts @@ -42,7 +42,7 @@ describe('normalizeDocsPluginOptions', () => { it('accepts correctly defined user options', () => { const userOptions: Options = { path: 'my-docs', // Path to data on filesystem, relative to site dir. - routeBasePath: 'my-docs', // URL Route. + routeBasePath: '/my-docs', // URL Route. tagsBasePath: 'tags', // URL Tags Route. include: ['**/*.{md,mdx}'], // Extensions to include. exclude: GlobExcludeDefault, diff --git a/packages/docusaurus-plugin-content-docs/src/options.ts b/packages/docusaurus-plugin-content-docs/src/options.ts index 01d97d7684..a7c2e1d1e1 100644 --- a/packages/docusaurus-plugin-content-docs/src/options.ts +++ b/packages/docusaurus-plugin-content-docs/src/options.ts @@ -11,6 +11,7 @@ import { RemarkPluginsSchema, RehypePluginsSchema, AdmonitionsSchema, + RouteBasePathSchema, URISchema, } from '@docusaurus/utils-validation'; import {GlobExcludeDefault} from '@docusaurus/utils'; @@ -73,10 +74,7 @@ const OptionsSchema = Joi.object({ editUrl: Joi.alternatives().try(URISchema, Joi.function()), editCurrentVersion: Joi.boolean().default(DEFAULT_OPTIONS.editCurrentVersion), editLocalizedFiles: Joi.boolean().default(DEFAULT_OPTIONS.editLocalizedFiles), - routeBasePath: Joi.string() - // '' not allowed, see https://github.com/facebook/docusaurus/issues/3374 - // .allow('') "" - .default(DEFAULT_OPTIONS.routeBasePath), + routeBasePath: RouteBasePathSchema.default(DEFAULT_OPTIONS.routeBasePath), tagsBasePath: Joi.string().default(DEFAULT_OPTIONS.tagsBasePath), // @ts-expect-error: deprecated homePageId: Joi.any().forbidden().messages({ diff --git a/packages/docusaurus-plugin-content-pages/src/__tests__/options.test.ts b/packages/docusaurus-plugin-content-pages/src/__tests__/options.test.ts index d92d691e7d..4112452f82 100644 --- a/packages/docusaurus-plugin-content-pages/src/__tests__/options.test.ts +++ b/packages/docusaurus-plugin-content-pages/src/__tests__/options.test.ts @@ -33,7 +33,7 @@ describe('normalizePagesPluginOptions', () => { it('accepts correctly defined user options', () => { const userOptions = { path: 'src/my-pages', - routeBasePath: 'my-pages', + routeBasePath: '/my-pages', include: ['**/*.{js,jsx,ts,tsx}'], exclude: ['**/$*/'], }; @@ -51,4 +51,15 @@ describe('normalizePagesPluginOptions', () => { }); }).toThrowErrorMatchingInlineSnapshot(`""path" must be a string"`); }); + + it('empty routeBasePath replace default path("/")', () => { + expect( + testValidate({ + routeBasePath: '', + }), + ).toEqual({ + ...defaultOptions, + routeBasePath: '/', + }); + }); }); diff --git a/packages/docusaurus-plugin-content-pages/src/options.ts b/packages/docusaurus-plugin-content-pages/src/options.ts index cce458f71d..014f07f119 100644 --- a/packages/docusaurus-plugin-content-pages/src/options.ts +++ b/packages/docusaurus-plugin-content-pages/src/options.ts @@ -10,6 +10,7 @@ import { RemarkPluginsSchema, RehypePluginsSchema, AdmonitionsSchema, + RouteBasePathSchema, } from '@docusaurus/utils-validation'; import {GlobExcludeDefault} from '@docusaurus/utils'; import type {OptionValidationContext} from '@docusaurus/types'; @@ -30,7 +31,7 @@ export const DEFAULT_OPTIONS: PluginOptions = { const PluginOptionSchema = Joi.object({ path: Joi.string().default(DEFAULT_OPTIONS.path), - routeBasePath: Joi.string().default(DEFAULT_OPTIONS.routeBasePath), + routeBasePath: RouteBasePathSchema.default(DEFAULT_OPTIONS.routeBasePath), include: Joi.array().items(Joi.string()).default(DEFAULT_OPTIONS.include), exclude: Joi.array().items(Joi.string()).default(DEFAULT_OPTIONS.exclude), mdxPageComponent: Joi.string().default(DEFAULT_OPTIONS.mdxPageComponent), diff --git a/packages/docusaurus-utils-validation/src/__tests__/__snapshots__/validationSchemas.test.ts.snap b/packages/docusaurus-utils-validation/src/__tests__/__snapshots__/validationSchemas.test.ts.snap index 4488ab4bdb..c0d9d3dd2a 100644 --- a/packages/docusaurus-utils-validation/src/__tests__/__snapshots__/validationSchemas.test.ts.snap +++ b/packages/docusaurus-utils-validation/src/__tests__/__snapshots__/validationSchemas.test.ts.snap @@ -130,4 +130,12 @@ exports[`validation schemas remarkPluginsSchema: for value=false 1`] = `""value" exports[`validation schemas remarkPluginsSchema: for value=null 1`] = `""value" must be an array"`; +exports[`validation schemas routeBasePathSchema: for value=[] 1`] = `""value" must be a string"`; + +exports[`validation schemas routeBasePathSchema: for value={} 1`] = `""value" must be a string"`; + +exports[`validation schemas routeBasePathSchema: for value=3 1`] = `""value" must be a string"`; + +exports[`validation schemas routeBasePathSchema: for value=null 1`] = `""value" must be a string"`; + exports[`validation schemas uRISchema: for value="spaces are invalid in a URL" 1`] = `""value" does not look like a valid url (value='')"`; diff --git a/packages/docusaurus-utils-validation/src/__tests__/validationSchemas.test.ts b/packages/docusaurus-utils-validation/src/__tests__/validationSchemas.test.ts index 7c8d3e42a1..7028560736 100644 --- a/packages/docusaurus-utils-validation/src/__tests__/validationSchemas.test.ts +++ b/packages/docusaurus-utils-validation/src/__tests__/validationSchemas.test.ts @@ -14,6 +14,7 @@ import { PluginIdSchema, URISchema, PathnameSchema, + RouteBasePathSchema, ContentVisibilitySchema, } from '../validationSchemas'; @@ -24,8 +25,9 @@ function createTestHelpers({ schema: Joi.Schema; defaultValue?: unknown; }) { - function testOK(value: unknown) { - expect(Joi.attempt(value, schema)).toEqual(value ?? defaultValue); + function testOK(value: unknown, options?: {normalizedValue?: unknown}) { + const expectedValue = options?.normalizedValue ?? value ?? defaultValue; + expect(Joi.attempt(value, schema)).toEqual(expectedValue); } function testFail(value: unknown) { @@ -168,6 +170,29 @@ describe('validation schemas', () => { testFail('https://github.com/foo'); }); + it('routeBasePathSchema', () => { + const {testFail, testOK} = createTestHelpers({ + schema: RouteBasePathSchema, + defaultValue: undefined, + }); + + testOK('', {normalizedValue: '/'}); + testOK('/'); + testOK('/foo', {normalizedValue: '/foo'}); + testOK('foo', {normalizedValue: '/foo'}); + testOK('blog', {normalizedValue: '/blog'}); + testOK('blog/', {normalizedValue: '/blog/'}); + testOK('prefix/blog', {normalizedValue: '/prefix/blog'}); + testOK('prefix/blog/', {normalizedValue: '/prefix/blog/'}); + testOK('/prefix/blog', {normalizedValue: '/prefix/blog'}); + testOK(undefined); + + testFail(3); + testFail([]); + testFail(null); + testFail({}); + }); + it('contentVisibilitySchema', () => { const {testFail, testOK} = createTestHelpers({ schema: ContentVisibilitySchema, diff --git a/packages/docusaurus-utils-validation/src/index.ts b/packages/docusaurus-utils-validation/src/index.ts index 8d3fd6566c..8c1dfb5ae0 100644 --- a/packages/docusaurus-utils-validation/src/index.ts +++ b/packages/docusaurus-utils-validation/src/index.ts @@ -20,6 +20,7 @@ export { RemarkPluginsSchema, RehypePluginsSchema, AdmonitionsSchema, + RouteBasePathSchema, URISchema, PathnameSchema, FrontMatterTagsSchema, diff --git a/packages/docusaurus-utils-validation/src/validationSchemas.ts b/packages/docusaurus-utils-validation/src/validationSchemas.ts index f5e29cf82e..0044855aee 100644 --- a/packages/docusaurus-utils-validation/src/validationSchemas.ts +++ b/packages/docusaurus-utils-validation/src/validationSchemas.ts @@ -5,7 +5,12 @@ * LICENSE file in the root directory of this source tree. */ -import {isValidPathname, DEFAULT_PLUGIN_ID, type Tag} from '@docusaurus/utils'; +import { + isValidPathname, + DEFAULT_PLUGIN_ID, + type Tag, + addLeadingSlash, +} from '@docusaurus/utils'; import Joi from './Joi'; import {JoiFrontMatter} from './JoiFrontMatter'; @@ -96,6 +101,26 @@ export const PathnameSchema = Joi.string() '{{#label}} is not a valid pathname. Pathname should start with slash and not contain any domain or query string.', ); +// Normalized schema for url path segments: baseUrl + routeBasePath... +// Note we only add a leading slash +// we don't always want to enforce a trailing slash on urls such as /docs +// +// Examples: +// '' => '/' +// 'docs' => '/docs' +// '/docs' => '/docs' +// 'docs/' => '/docs' +// 'prefix/docs' => '/prefix/docs' +// TODO tighter validation: not all strings are valid path segments +export const RouteBasePathSchema = Joi + // Weird Joi trick needed, otherwise value '' is not normalized... + .alternatives() + .try(Joi.string().required().allow('')) + .custom((value: string) => + // /!\ do not add trailing slash here + addLeadingSlash(value), + ); + const FrontMatterTagSchema = JoiFrontMatter.alternatives() .try( JoiFrontMatter.string().required(), diff --git a/packages/docusaurus/src/server/__tests__/configValidation.test.ts b/packages/docusaurus/src/server/__tests__/configValidation.test.ts index 81ed54f70e..e6e0bcd225 100644 --- a/packages/docusaurus/src/server/__tests__/configValidation.test.ts +++ b/packages/docusaurus/src/server/__tests__/configValidation.test.ts @@ -134,6 +134,11 @@ describe('normalizeConfig', () => { }); it('normalizes various base URLs', () => { + expect( + normalizeConfig({ + baseUrl: '', + }).baseUrl, + ).toBe('/'); expect( normalizeConfig({ baseUrl: 'noSlash', diff --git a/packages/docusaurus/src/server/configValidation.ts b/packages/docusaurus/src/server/configValidation.ts index 09246c3e1e..092a634677 100644 --- a/packages/docusaurus/src/server/configValidation.ts +++ b/packages/docusaurus/src/server/configValidation.ts @@ -180,7 +180,10 @@ const SiteUrlSchema = Joi.string() // TODO move to @docusaurus/utils-validation export const ConfigSchema = Joi.object({ - baseUrl: Joi.string() + baseUrl: Joi + // Weird Joi trick needed, otherwise value '' is not normalized... + .alternatives() + .try(Joi.string().required().allow('')) .required() .custom((value: string) => addLeadingSlash(addTrailingSlash(value))), baseUrlIssueBanner: Joi.boolean().default(DEFAULT_CONFIG.baseUrlIssueBanner),