feat(core): allow sourcing from multiple static directories (#4095)

* [WIP] Implementaion of multiple directory static sourcing

* Move default to validation

* Update test

* Refactor

* Port to MDX loader

* Fix

* Move dogfooding assets

* Doc writeup

* Restore assets

* Support absolute paths

* Dogfood absolute path

* Fix

* More tests

* Fix snapshots

Co-authored-by: Joshua Chen <sidachen2003@gmail.com>
This commit is contained in:
Oliver Ullman 2021-11-18 11:26:26 -03:00 committed by GitHub
parent 3f18c928bb
commit 1366c31201
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
43 changed files with 238 additions and 181 deletions

View file

@ -36,7 +36,8 @@ const DEFAULT_OPTIONS: RemarkAndRehypePluginOptions = {
}; };
type Options = RemarkAndRehypePluginOptions & { type Options = RemarkAndRehypePluginOptions & {
staticDir?: string; staticDirs: string[];
siteDir: string;
isMDXPartial?: (filePath: string) => boolean; isMDXPartial?: (filePath: string) => boolean;
isMDXPartialFrontMatterWarningDisabled?: boolean; isMDXPartialFrontMatterWarningDisabled?: boolean;
removeContentTitle?: boolean; removeContentTitle?: boolean;
@ -123,8 +124,15 @@ export default async function mdxLoader(
remarkPlugins: [ remarkPlugins: [
...(reqOptions.beforeDefaultRemarkPlugins || []), ...(reqOptions.beforeDefaultRemarkPlugins || []),
...DEFAULT_OPTIONS.remarkPlugins, ...DEFAULT_OPTIONS.remarkPlugins,
[transformImage, {staticDir: reqOptions.staticDir, filePath}], [transformImage, {staticDirs: reqOptions.staticDirs, filePath}],
[transformLinks, {staticDir: reqOptions.staticDir, filePath}], [
transformLinks,
{
staticDirs: reqOptions.staticDirs,
filePath,
siteDir: reqOptions.siteDir,
},
],
...(reqOptions.remarkPlugins || []), ...(reqOptions.remarkPlugins || []),
], ],
rehypePlugins: [ rehypePlugins: [

View file

@ -0,0 +1,19 @@
![img](https://example.com/img.png)
![](./static/img.png)
![img](./static/img.png)
![img from second static folder](/img2.png)
![img from second static folder](./static2/img2.png)
![img](./static/img.png 'Title') ![img](/img.png)
![img with "quotes"](./static/img.png ''Quoted' title')
## Heading
```md
![img](./static/img.png)
```

View file

@ -1,8 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`transformImage plugin fail if image does not exist 1`] = `"Image packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/fixtures/img/doesNotExist.png used in packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/fixtures/fail.md not found."`; exports[`transformImage plugin fail if image does not exist 1`] = `"Image packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/__fixtures__/static/img/doesNotExist.png or packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/__fixtures__/static2/img/doesNotExist.png used in packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/__fixtures__/fail.md not found."`;
exports[`transformImage plugin fail if image url is absent 1`] = `"Markdown image URL is mandatory in \\"packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/fixtures/noUrl.md\\" file"`; exports[`transformImage plugin fail if image url is absent 1`] = `"Markdown image URL is mandatory in \\"packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/__fixtures__/noUrl.md\\" file"`;
exports[`transformImage plugin pathname protocol 1`] = ` exports[`transformImage plugin pathname protocol 1`] = `
"![img](/img/unchecked.png) "![img](/img/unchecked.png)
@ -12,20 +12,22 @@ exports[`transformImage plugin pathname protocol 1`] = `
exports[`transformImage plugin transform md images to <img /> 1`] = ` exports[`transformImage plugin transform md images to <img /> 1`] = `
"![img](https://example.com/img.png) "![img](https://example.com/img.png)
<img src={require(\\"!url-loader?limit=10000&name=assets/images/[name]-[hash].[ext]&fallback=file-loader!./img.png\\").default} /> <img src={require(\\"!url-loader?limit=10000&name=assets/images/[name]-[hash].[ext]&fallback=file-loader!./static/img.png\\").default} />
<img alt={\\"img\\"} src={require(\\"!url-loader?limit=10000&name=assets/images/[name]-[hash].[ext]&fallback=file-loader!./img.png\\").default} /> <img alt={\\"img\\"} src={require(\\"!url-loader?limit=10000&name=assets/images/[name]-[hash].[ext]&fallback=file-loader!./static/img.png\\").default} />
<img alt={\\"img\\"} src={require(\\"!url-loader?limit=10000&name=assets/images/[name]-[hash].[ext]&fallback=file-loader!./img.png\\").default} title=\\"Title\\" /> <img alt={\\"img\\"} src={require(\\"!url-loader?limit=10000&name=assets/images/[name]-[hash].[ext]&fallback=file-loader!packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/fixtures/img.png\\").default} /> <img alt={\\"img from second static folder\\"} src={require(\\"!url-loader?limit=10000&name=assets/images/[name]-[hash].[ext]&fallback=file-loader!packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/__fixtures__/static2/img2.png\\").default} />
<img alt={\\"img with &quot;quotes&quot;\\"} src={require(\\"!url-loader?limit=10000&name=assets/images/[name]-[hash].[ext]&fallback=file-loader!./img.png\\").default} title=\\"&#39;Quoted&#39; title\\" /> <img alt={\\"img from second static folder\\"} src={require(\\"!url-loader?limit=10000&name=assets/images/[name]-[hash].[ext]&fallback=file-loader!./static2/img2.png\\").default} />
<img alt={\\"img\\"} src={require(\\"!url-loader?limit=10000&name=assets/images/[name]-[hash].[ext]&fallback=file-loader!./static/img.png\\").default} title=\\"Title\\" /> <img alt={\\"img\\"} src={require(\\"!url-loader?limit=10000&name=assets/images/[name]-[hash].[ext]&fallback=file-loader!packages/docusaurus-mdx-loader/src/remark/transformImage/__tests__/__fixtures__/static/img.png\\").default} />
<img alt={\\"img with &quot;quotes&quot;\\"} src={require(\\"!url-loader?limit=10000&name=assets/images/[name]-[hash].[ext]&fallback=file-loader!./static/img.png\\").default} title=\\"&#39;Quoted&#39; title\\" />
## Heading ## Heading
\`\`\`md \`\`\`md
![img](./img.png) ![img](./static/img.png)
\`\`\` \`\`\`
<img alt={\\"img\\"} src={require(\\"!url-loader?limit=10000&name=assets/images/[name]-[hash].[ext]&fallback=file-loader!./img.png\\").default} />
" "
`; `;

View file

@ -1,17 +0,0 @@
![img](https://example.com/img.png)
![](./img.png)
![img](./img.png)
![img](./img.png 'Title') ![img](/img.png)
![img with "quotes"](./img.png ''Quoted' title')
## Heading
```md
![img](./img.png)
```
![img](img.png)

View file

@ -5,7 +5,7 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import {join, relative} from 'path'; import path from 'path';
import remark from 'remark'; import remark from 'remark';
import mdx from 'remark-mdx'; import mdx from 'remark-mdx';
import vfile from 'to-vfile'; import vfile from 'to-vfile';
@ -13,47 +13,48 @@ import plugin from '../index';
import headings from '../../headings/index'; import headings from '../../headings/index';
const processFixture = async (name, options) => { const processFixture = async (name, options) => {
const path = join(__dirname, 'fixtures', `${name}.md`); const filePath = path.join(__dirname, `__fixtures__/${name}.md`);
const file = await vfile.read(path); const file = await vfile.read(filePath);
const result = await remark() const result = await remark()
.use(headings) .use(headings)
.use(mdx) .use(mdx)
.use(plugin, {...options, filePath: path}) .use(plugin, {...options, filePath})
.process(file); .process(file);
return result.toString(); return result.toString();
}; };
// avoid hardcoding absolute const staticDirs = [
const staticDir = `./${relative(process.cwd(), join(__dirname, 'fixtures'))}`; // avoid hardcoding absolute in the snapshot
`./${path.relative(
process.cwd(),
path.join(__dirname, '__fixtures__/static'),
)}`,
`./${path.relative(
process.cwd(),
path.join(__dirname, '__fixtures__/static2'),
)}`,
];
describe('transformImage plugin', () => { describe('transformImage plugin', () => {
test('fail if image does not exist', async () => { test('fail if image does not exist', async () => {
await expect( await expect(
processFixture('fail', { processFixture('fail', {staticDirs}),
staticDir,
}),
).rejects.toThrowErrorMatchingSnapshot(); ).rejects.toThrowErrorMatchingSnapshot();
}); });
test('fail if image url is absent', async () => { test('fail if image url is absent', async () => {
await expect( await expect(
processFixture('noUrl', { processFixture('noUrl', {staticDirs}),
staticDir,
}),
).rejects.toThrowErrorMatchingSnapshot(); ).rejects.toThrowErrorMatchingSnapshot();
}); });
test('transform md images to <img />', async () => { test('transform md images to <img />', async () => {
const result = await processFixture('img', { const result = await processFixture('img', {staticDirs});
staticDir,
});
expect(result).toMatchSnapshot(); expect(result).toMatchSnapshot();
}); });
test('pathname protocol', async () => { test('pathname protocol', async () => {
const result = await processFixture('pathname', { const result = await processFixture('pathname', {staticDirs});
staticDir,
});
expect(result).toMatchSnapshot(); expect(result).toMatchSnapshot();
}); });
}); });

View file

@ -25,7 +25,7 @@ const {
interface PluginOptions { interface PluginOptions {
filePath: string; filePath: string;
staticDir: string; staticDirs: string[];
} }
const createJSX = (node: Image, pathUrl: string) => { const createJSX = (node: Image, pathUrl: string) => {
@ -63,9 +63,25 @@ async function ensureImageFileExist(imagePath: string, sourceFilePath: string) {
} }
} }
async function findImage(possiblePaths: string[], sourceFilePath: string) {
// eslint-disable-next-line no-restricted-syntax
for (const possiblePath of possiblePaths) {
if (await fs.pathExists(possiblePath)) {
return possiblePath;
}
}
throw new Error(
`Image ${possiblePaths
.map((p) => toMessageRelativeFilePath(p))
.join(' or ')} used in ${toMessageRelativeFilePath(
sourceFilePath,
)} not found.`,
);
}
async function processImageNode( async function processImageNode(
node: Image, node: Image,
{filePath, staticDir}: PluginOptions, {filePath, staticDirs}: PluginOptions,
) { ) {
if (!node.url) { if (!node.url) {
throw new Error( throw new Error(
@ -88,9 +104,11 @@ async function processImageNode(
// images without protocol // images without protocol
else if (path.isAbsolute(node.url)) { else if (path.isAbsolute(node.url)) {
// absolute paths are expected to exist in the static folder // absolute paths are expected to exist in the static folder
const expectedImagePath = path.join(staticDir, node.url); const possibleImagePaths = staticDirs.map((dir) =>
await ensureImageFileExist(expectedImagePath, filePath); path.join(dir, node.url),
createJSX(node, posixPath(expectedImagePath)); );
const imagePath = await findImage(possibleImagePaths, filePath);
createJSX(node, posixPath(imagePath));
} }
// We try to convert image urls without protocol to images with require calls // We try to convert image urls without protocol to images with require calls
// going through webpack ensures that image assets exist at build time // going through webpack ensures that image assets exist at build time

View file

@ -20,6 +20,8 @@
[asset](asset.pdf) [asset](asset.pdf)
[asset2](/asset2.pdf)
[staticAsset.pdf](/staticAsset.pdf) [staticAsset.pdf](/staticAsset.pdf)
[@site/static/staticAsset.pdf](@site/static/staticAsset.pdf) [@site/static/staticAsset.pdf](@site/static/staticAsset.pdf)

View file

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`transformAsset plugin fail if asset url is absent 1`] = `"Markdown link URL is mandatory in \\"packages/docusaurus-mdx-loader/src/remark/transformLinks/__tests__/fixtures/noUrl.md\\" file (title: asset, line: 1)."`; exports[`transformAsset plugin fail if asset url is absent 1`] = `"Markdown link URL is mandatory in \\"packages/docusaurus-mdx-loader/src/remark/transformLinks/__tests__/__fixtures__/noUrl.md\\" file (title: asset, line: 1)."`;
exports[`transformAsset plugin pathname protocol 1`] = ` exports[`transformAsset plugin pathname protocol 1`] = `
"[asset](pathname:///asset/unchecked.pdf) "[asset](pathname:///asset/unchecked.pdf)
@ -30,6 +30,8 @@ exports[`transformAsset plugin transform md links to <a /> 1`] = `
<a target=\\"_blank\\" href={require('!file-loader?name=assets/files/[name]-[hash].[ext]!./asset.pdf').default}>asset</a> <a target=\\"_blank\\" href={require('!file-loader?name=assets/files/[name]-[hash].[ext]!./asset.pdf').default}>asset</a>
<a target=\\"_blank\\" href={require('!file-loader?name=assets/files/[name]-[hash].[ext]!./static2/asset2.pdf').default}>asset2</a>
<a target=\\"_blank\\" href={require('!file-loader?name=assets/files/[name]-[hash].[ext]!./static/staticAsset.pdf').default}>staticAsset.pdf</a> <a target=\\"_blank\\" href={require('!file-loader?name=assets/files/[name]-[hash].[ext]!./static/staticAsset.pdf').default}>staticAsset.pdf</a>
<a target=\\"_blank\\" href={require('!file-loader?name=assets/files/[name]-[hash].[ext]!./static/staticAsset.pdf').default}>@site/static/staticAsset.pdf</a> <a target=\\"_blank\\" href={require('!file-loader?name=assets/files/[name]-[hash].[ext]!./static/staticAsset.pdf').default}>@site/static/staticAsset.pdf</a>

View file

@ -5,21 +5,29 @@
* LICENSE file in the root directory of this source tree. * LICENSE file in the root directory of this source tree.
*/ */
import {join} from 'path'; import path from 'path';
import remark from 'remark'; import remark from 'remark';
import mdx from 'remark-mdx'; import mdx from 'remark-mdx';
import vfile from 'to-vfile'; import vfile from 'to-vfile';
import plugin from '..'; import plugin from '..';
import transformImage from '../../transformImage'; import transformImage from '../../transformImage';
const processFixture = async (name, options) => { const processFixture = async (name: string, options?) => {
const path = join(__dirname, 'fixtures', `${name}.md`); const filePath = path.join(__dirname, `__fixtures__/${name}.md`);
const staticDir = join(__dirname, 'fixtures', 'static'); const staticDirs = [
const file = await vfile.read(path); path.join(__dirname, '__fixtures__/static'),
path.join(__dirname, '__fixtures__/static2'),
];
const file = await vfile.read(filePath);
const result = await remark() const result = await remark()
.use(mdx) .use(mdx)
.use(transformImage, {...options, filePath: path, staticDir}) .use(transformImage, {...options, filePath, staticDirs})
.use(plugin, {...options, filePath: path, staticDir}) .use(plugin, {
...options,
filePath,
staticDirs,
siteDir: path.join(__dirname, '__fixtures__'),
})
.process(file); .process(file);
return result.toString(); return result.toString();

View file

@ -27,7 +27,8 @@ const hashRegex = /#.*$/;
interface PluginOptions { interface PluginOptions {
filePath: string; filePath: string;
staticDir: string; staticDirs: string[];
siteDir: string;
} }
async function ensureAssetFileExist( async function ensureAssetFileExist(
@ -81,11 +82,10 @@ function toAssetRequireNode({
// If the link looks like an asset link, we'll link to the asset, // If the link looks like an asset link, we'll link to the asset,
// and use a require("assetUrl") (using webpack url-loader/file-loader) // and use a require("assetUrl") (using webpack url-loader/file-loader)
// instead of navigating to such link // instead of navigating to such link
async function convertToAssetLinkIfNeeded({ async function convertToAssetLinkIfNeeded(
node, node: Link,
staticDir, {filePath, siteDir, staticDirs}: PluginOptions,
filePath, ) {
}: {node: Link} & PluginOptions) {
const assetPath = node.url.replace(hashRegex, ''); const assetPath = node.url.replace(hashRegex, '');
const hasSiteAlias = assetPath.startsWith('@site/'); const hasSiteAlias = assetPath.startsWith('@site/');
@ -107,7 +107,6 @@ async function convertToAssetLinkIfNeeded({
} }
if (assetPath.startsWith('@site/')) { if (assetPath.startsWith('@site/')) {
const siteDir = path.join(staticDir, '..');
const fileSystemAssetPath = path.join( const fileSystemAssetPath = path.join(
siteDir, siteDir,
assetPath.replace('@site/', ''), assetPath.replace('@site/', ''),
@ -115,9 +114,13 @@ async function convertToAssetLinkIfNeeded({
await ensureAssetFileExist(fileSystemAssetPath, filePath); await ensureAssetFileExist(fileSystemAssetPath, filePath);
toAssetLinkNode(fileSystemAssetPath); toAssetLinkNode(fileSystemAssetPath);
} else if (path.isAbsolute(assetPath)) { } else if (path.isAbsolute(assetPath)) {
const fileSystemAssetPath = path.join(staticDir, assetPath); // eslint-disable-next-line no-restricted-syntax
if (await fs.pathExists(fileSystemAssetPath)) { for (const staticDir of staticDirs) {
toAssetLinkNode(fileSystemAssetPath); const fileSystemAssetPath = path.join(staticDir, assetPath);
if (await fs.pathExists(fileSystemAssetPath)) {
toAssetLinkNode(fileSystemAssetPath);
return;
}
} }
} else { } else {
const fileSystemAssetPath = path.join(path.dirname(filePath), assetPath); const fileSystemAssetPath = path.join(path.dirname(filePath), assetPath);
@ -127,11 +130,7 @@ async function convertToAssetLinkIfNeeded({
} }
} }
async function processLinkNode({ async function processLinkNode(node: Link, options: PluginOptions) {
node,
filePath,
staticDir,
}: {node: Link} & PluginOptions) {
if (!node.url) { if (!node.url) {
// try to improve error feedback // try to improve error feedback
// see https://github.com/facebook/docusaurus/issues/3309#issuecomment-690371675 // see https://github.com/facebook/docusaurus/issues/3309#issuecomment-690371675
@ -139,7 +138,7 @@ async function processLinkNode({
const line = node?.position?.start?.line || '?'; const line = node?.position?.start?.line || '?';
throw new Error( throw new Error(
`Markdown link URL is mandatory in "${toMessageRelativeFilePath( `Markdown link URL is mandatory in "${toMessageRelativeFilePath(
filePath, options.filePath,
)}" file (title: ${title}, line: ${line}).`, )}" file (title: ${title}, line: ${line}).`,
); );
} }
@ -149,14 +148,14 @@ async function processLinkNode({
return; return;
} }
await convertToAssetLinkIfNeeded({node, staticDir, filePath}); await convertToAssetLinkIfNeeded(node, options);
} }
const plugin: Plugin<[PluginOptions]> = (options) => { const plugin: Plugin<[PluginOptions]> = (options) => {
const transformer: Transformer = async (root) => { const transformer: Transformer = async (root) => {
const promises: Promise<void>[] = []; const promises: Promise<void>[] = [];
visit(root, 'link', (node: Link) => { visit(root, 'link', (node: Link) => {
promises.push(processLinkNode({node, ...options})); promises.push(processLinkNode(node, options));
}); });
await Promise.all(promises); await Promise.all(promises);
}; };

View file

@ -17,10 +17,7 @@ import {
addTrailingPathSeparator, addTrailingPathSeparator,
createAbsoluteFilePathMatcher, createAbsoluteFilePathMatcher,
} from '@docusaurus/utils'; } from '@docusaurus/utils';
import { import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants';
STATIC_DIR_NAME,
DEFAULT_PLUGIN_ID,
} from '@docusaurus/core/lib/constants';
import {translateContent, getTranslationFiles} from './translations'; import {translateContent, getTranslationFiles} from './translations';
import { import {
@ -465,7 +462,10 @@ export default function pluginContentBlog(
rehypePlugins, rehypePlugins,
beforeDefaultRemarkPlugins, beforeDefaultRemarkPlugins,
beforeDefaultRehypePlugins, beforeDefaultRehypePlugins,
staticDir: path.join(siteDir, STATIC_DIR_NAME), staticDirs: siteConfig.staticDirectories.map((dir) =>
path.resolve(siteDir, dir),
),
siteDir,
isMDXPartial: createAbsoluteFilePathMatcher( isMDXPartial: createAbsoluteFilePathMatcher(
options.exclude, options.exclude,
contentDirs, contentDirs,

View file

@ -7,10 +7,7 @@
import path from 'path'; import path from 'path';
import { import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants';
STATIC_DIR_NAME,
DEFAULT_PLUGIN_ID,
} from '@docusaurus/core/lib/constants';
import { import {
normalizeUrl, normalizeUrl,
docuHash, docuHash,
@ -397,7 +394,10 @@ export default function pluginContentDocs(
rehypePlugins, rehypePlugins,
beforeDefaultRehypePlugins, beforeDefaultRehypePlugins,
beforeDefaultRemarkPlugins, beforeDefaultRemarkPlugins,
staticDir: path.join(siteDir, STATIC_DIR_NAME), staticDirs: siteConfig.staticDirectories.map((dir) =>
path.resolve(siteDir, dir),
),
siteDir,
isMDXPartial: createAbsoluteFilePathMatcher( isMDXPartial: createAbsoluteFilePathMatcher(
options.exclude, options.exclude,
contentDirs, contentDirs,

View file

@ -29,10 +29,7 @@ import {
import {Configuration} from 'webpack'; import {Configuration} from 'webpack';
import admonitions from 'remark-admonitions'; import admonitions from 'remark-admonitions';
import {PluginOptionSchema} from './pluginOptionSchema'; import {PluginOptionSchema} from './pluginOptionSchema';
import { import {DEFAULT_PLUGIN_ID} from '@docusaurus/core/lib/constants';
DEFAULT_PLUGIN_ID,
STATIC_DIR_NAME,
} from '@docusaurus/core/lib/constants';
import { import {
PluginOptions, PluginOptions,
@ -209,7 +206,10 @@ export default function pluginContentPages(
rehypePlugins, rehypePlugins,
beforeDefaultRehypePlugins, beforeDefaultRehypePlugins,
beforeDefaultRemarkPlugins, beforeDefaultRemarkPlugins,
staticDir: path.join(siteDir, STATIC_DIR_NAME), staticDirs: siteConfig.staticDirectories.map((dir) =>
path.resolve(siteDir, dir),
),
siteDir,
isMDXPartial: createAbsoluteFilePathMatcher( isMDXPartial: createAbsoluteFilePathMatcher(
options.exclude, options.exclude,
contentDirs, contentDirs,

View file

@ -10,20 +10,15 @@ import remark from 'remark';
// TODO change to `../index` after migrating to ESM // TODO change to `../index` after migrating to ESM
import npm2yarn from '../../lib/index'; import npm2yarn from '../../lib/index';
import vfile from 'to-vfile'; import vfile from 'to-vfile';
import {join, relative} from 'path'; import path from 'path';
import mdx from 'remark-mdx'; import mdx from 'remark-mdx';
const staticDir = `./${relative(process.cwd(), join(__dirname, 'fixtures'))}`; const processFixture = async (name: string, options?: {sync?: boolean}) => {
const filePath = path.join(__dirname, 'fixtures', `${name}.md`);
const processFixture = async ( const file = await vfile.read(filePath);
name: string,
options: {sync?: boolean; staticDir: string},
) => {
const path = join(__dirname, 'fixtures', `${name}.md`);
const file = await vfile.read(path);
const result = await remark() const result = await remark()
.use(mdx) .use(mdx)
.use(npm2yarn, {...options, filePath: path}) .use(npm2yarn, {...options, filePath})
.process(file); .process(file);
return result.toString(); return result.toString();
@ -31,41 +26,31 @@ const processFixture = async (
describe('npm2yarn plugin', () => { describe('npm2yarn plugin', () => {
test('test: installation file', async () => { test('test: installation file', async () => {
const result = await processFixture('installation', { const result = await processFixture('installation');
staticDir,
});
expect(result).toMatchSnapshot(); expect(result).toMatchSnapshot();
}); });
test('test: plugin file', async () => { test('test: plugin file', async () => {
const result = await processFixture('plugin', { const result = await processFixture('plugin');
staticDir,
});
expect(result).toMatchSnapshot(); expect(result).toMatchSnapshot();
}); });
test('test: language was not setted', async () => { test('test: language was not setted', async () => {
const result = await processFixture('syntax-not-properly-set', { const result = await processFixture('syntax-not-properly-set');
staticDir,
});
expect(result).toMatchSnapshot(); expect(result).toMatchSnapshot();
}); });
test('test: already imported tabs components above are not re-imported', async () => { test('test: already imported tabs components above are not re-imported', async () => {
const result = await processFixture('import-tabs-above', { const result = await processFixture('import-tabs-above');
staticDir,
});
expect(result).toMatchSnapshot(); expect(result).toMatchSnapshot();
}); });
test('test: already imported tabs components below are not re-imported', async () => { test('test: already imported tabs components below are not re-imported', async () => {
const result = await processFixture('import-tabs-below', { const result = await processFixture('import-tabs-below');
staticDir,
});
expect(result).toMatchSnapshot(); expect(result).toMatchSnapshot();
}); });

View file

@ -58,6 +58,7 @@ export interface DocusaurusConfig {
)[]; )[];
clientModules?: string[]; clientModules?: string[];
ssrTemplate?: string; ssrTemplate?: string;
staticDirectories: string[];
stylesheets?: ( stylesheets?: (
| string | string
| { | {

View file

@ -13,7 +13,6 @@ import ReactLoadableSSRAddon from 'react-loadable-ssr-addon-v5-slorber';
import {Configuration} from 'webpack'; import {Configuration} from 'webpack';
import {BundleAnalyzerPlugin} from 'webpack-bundle-analyzer'; import {BundleAnalyzerPlugin} from 'webpack-bundle-analyzer';
import merge from 'webpack-merge'; import merge from 'webpack-merge';
import {STATIC_DIR_NAME} from '../constants';
import {load, loadContext} from '../server'; import {load, loadContext} from '../server';
import {handleBrokenLinks} from '../server/brokenLinks'; import {handleBrokenLinks} from '../server/brokenLinks';
@ -129,7 +128,7 @@ async function buildLocale({
outDir, outDir,
generatedFilesDir, generatedFilesDir,
plugins, plugins,
siteConfig: {baseUrl, onBrokenLinks}, siteConfig: {baseUrl, onBrokenLinks, staticDirectories},
routes, routes,
} = props; } = props;
@ -162,21 +161,16 @@ async function buildLocale({
}, },
}); });
const staticDir = path.resolve(siteDir, STATIC_DIR_NAME); serverConfig = merge(serverConfig, {
if (await fs.pathExists(staticDir)) { plugins: [
serverConfig = merge(serverConfig, { new CopyWebpackPlugin({
plugins: [ patterns: staticDirectories
new CopyWebpackPlugin({ .map((dir) => path.resolve(siteDir, dir))
patterns: [ .filter(fs.existsSync)
{ .map((dir) => ({from: dir, to: outDir})),
from: staticDir, }),
to: outDir, ],
}, });
],
}),
],
});
}
// Plugin Lifecycle - configureWebpack and configurePostCss. // Plugin Lifecycle - configureWebpack and configurePostCss.
plugins.forEach((plugin) => { plugins.forEach((plugin) => {

View file

@ -20,7 +20,6 @@ import WebpackDevServer from 'webpack-dev-server';
import merge from 'webpack-merge'; import merge from 'webpack-merge';
import {load} from '../server'; import {load} from '../server';
import {StartCLIOptions} from '@docusaurus/types'; import {StartCLIOptions} from '@docusaurus/types';
import {STATIC_DIR_NAME} from '../constants';
import createClientConfig from '../webpack/client'; import createClientConfig from '../webpack/client';
import { import {
applyConfigureWebpack, applyConfigureWebpack,
@ -187,9 +186,9 @@ export default async function start(
// Reduce log verbosity, see https://github.com/facebook/docusaurus/pull/5420#issuecomment-906613105 // Reduce log verbosity, see https://github.com/facebook/docusaurus/pull/5420#issuecomment-906613105
stats: 'summary', stats: 'summary',
}, },
static: { static: siteConfig.staticDirectories.map((dir) => ({
publicPath: baseUrl, publicPath: baseUrl,
directory: path.resolve(siteDir, STATIC_DIR_NAME), directory: path.resolve(siteDir, dir),
watch: { watch: {
// Useful options for our own monorepo using symlinks! // Useful options for our own monorepo using symlinks!
// See https://github.com/webpack/webpack/issues/11612#issuecomment-879259806 // See https://github.com/webpack/webpack/issues/11612#issuecomment-879259806
@ -197,7 +196,7 @@ export default async function start(
ignored: /node_modules\/(?!@docusaurus)/, ignored: /node_modules\/(?!@docusaurus)/,
...{pollingOptions}, ...{pollingOptions},
}, },
}, })),
historyApiFallback: { historyApiFallback: {
rewrites: [{from: /\/*/, to: baseUrl}], rewrites: [{from: /\/*/, to: baseUrl}],
}, },

View file

@ -40,6 +40,9 @@ Object {
], ],
"presets": Array [], "presets": Array [],
"projectName": "hello", "projectName": "hello",
"staticDirectories": Array [
"static",
],
"tagline": "Hello World", "tagline": "Hello World",
"themeConfig": Object {}, "themeConfig": Object {},
"themes": Array [], "themes": Array [],

View file

@ -6,7 +6,7 @@
*/ */
import {DocusaurusConfig, I18nConfig} from '@docusaurus/types'; import {DocusaurusConfig, I18nConfig} from '@docusaurus/types';
import {DEFAULT_CONFIG_FILE_NAME} from '../constants'; import {DEFAULT_CONFIG_FILE_NAME, STATIC_DIR_NAME} from '../constants';
import { import {
Joi, Joi,
logValidationBugReportHint, logValidationBugReportHint,
@ -37,6 +37,7 @@ export const DEFAULT_CONFIG: Pick<
| 'titleDelimiter' | 'titleDelimiter'
| 'noIndex' | 'noIndex'
| 'baseUrlIssueBanner' | 'baseUrlIssueBanner'
| 'staticDirectories'
> = { > = {
i18n: DEFAULT_I18N_CONFIG, i18n: DEFAULT_I18N_CONFIG,
onBrokenLinks: 'throw', onBrokenLinks: 'throw',
@ -50,6 +51,7 @@ export const DEFAULT_CONFIG: Pick<
titleDelimiter: '|', titleDelimiter: '|',
noIndex: false, noIndex: false,
baseUrlIssueBanner: true, baseUrlIssueBanner: true,
staticDirectories: [STATIC_DIR_NAME],
}; };
const PluginSchema = Joi.alternatives() const PluginSchema = Joi.alternatives()
@ -142,6 +144,9 @@ export const ConfigSchema = Joi.object({
.equal('ignore', 'log', 'warn', 'error', 'throw') .equal('ignore', 'log', 'warn', 'error', 'throw')
.default(DEFAULT_CONFIG.onDuplicateRoutes), .default(DEFAULT_CONFIG.onDuplicateRoutes),
organizationName: Joi.string().allow(''), organizationName: Joi.string().allow(''),
staticDirectories: Joi.array()
.items(Joi.string())
.default(DEFAULT_CONFIG.staticDirectories),
projectName: Joi.string().allow(''), projectName: Joi.string().allow(''),
deploymentBranch: Joi.string().optional(), deploymentBranch: Joi.string().optional(),
customFields: Joi.object().unknown().default(DEFAULT_CONFIG.customFields), customFields: Joi.object().unknown().default(DEFAULT_CONFIG.customFields),

View file

@ -13,7 +13,6 @@ import {
DEFAULT_BUILD_DIR_NAME, DEFAULT_BUILD_DIR_NAME,
DEFAULT_CONFIG_FILE_NAME, DEFAULT_CONFIG_FILE_NAME,
GENERATED_FILES_DIR_NAME, GENERATED_FILES_DIR_NAME,
STATIC_DIR_NAME,
} from '../constants'; } from '../constants';
import loadClientModules from './client-modules'; import loadClientModules from './client-modules';
import loadConfig from './config'; import loadConfig from './config';
@ -193,7 +192,13 @@ function createBootstrapPlugin({
// Adds a "fallback" mdx loader for mdx files that are not processed by content plugins // Adds a "fallback" mdx loader for mdx files that are not processed by content plugins
// This allows to do things such as importing repo/README.md as a partial from another doc // This allows to do things such as importing repo/README.md as a partial from another doc
// Not ideal solution though, but good enough for now // Not ideal solution though, but good enough for now
function createMDXFallbackPlugin({siteDir}: {siteDir: string}): LoadedPlugin { function createMDXFallbackPlugin({
siteDir,
siteConfig,
}: {
siteDir: string;
siteConfig: DocusaurusConfig;
}): LoadedPlugin {
return { return {
name: 'docusaurus-mdx-fallback-plugin', name: 'docusaurus-mdx-fallback-plugin',
content: null, content: null,
@ -223,7 +228,10 @@ function createMDXFallbackPlugin({siteDir}: {siteDir: string}): LoadedPlugin {
{ {
loader: require.resolve('@docusaurus/mdx-loader'), loader: require.resolve('@docusaurus/mdx-loader'),
options: { options: {
staticDir: path.join(siteDir, STATIC_DIR_NAME), staticDirs: siteConfig.staticDirectories.map((dir) =>
path.resolve(siteDir, dir),
),
siteDir,
isMDXPartial: (_filename: string) => true, // External mdx files are always meant to be imported as partials isMDXPartial: (_filename: string) => true, // External mdx files are always meant to be imported as partials
isMDXPartialFrontMatterWarningDisabled: true, // External mdx files might have frontmatter, let's just disable the warning isMDXPartialFrontMatterWarningDisabled: true, // External mdx files might have frontmatter, let's just disable the warning
remarkPlugins: [admonitions], remarkPlugins: [admonitions],
@ -273,7 +281,7 @@ export async function load(
); );
plugins.push(createBootstrapPlugin({siteConfig})); plugins.push(createBootstrapPlugin({siteConfig}));
plugins.push(createMDXFallbackPlugin({siteDir})); plugins.push(createMDXFallbackPlugin({siteDir, siteConfig}));
// Load client modules. // Load client modules.
const clientModules = loadClientModules(plugins); const clientModules = loadClientModules(plugins);

View file

Before

Width:  |  Height:  |  Size: 5 KiB

After

Width:  |  Height:  |  Size: 5 KiB

View file

Before

Width:  |  Height:  |  Size: 301 KiB

After

Width:  |  Height:  |  Size: 301 KiB

View file

Before

Width:  |  Height:  |  Size: 5 KiB

After

Width:  |  Height:  |  Size: 5 KiB

View file

@ -6,13 +6,13 @@ This is a test page to see if Docusaurus markdown features are working properly
See [#3337](https://github.com/facebook/docusaurus/issues/3337) See [#3337](https://github.com/facebook/docusaurus/issues/3337)
- [/dogfooding/someFile.pdf](/dogfooding/someFile.pdf) - [/someFile.pdf](/someFile.pdf)
- [/dogfooding/someFile.xyz](/dogfooding/someFile.xyz) - [/someFile.xyz](/someFile.xyz)
- [@site/static/dogfooding/someFile.pdf](@site/static/dogfooding/someFile.pdf) - [@site/\_dogfooding/\_asset-tests/someFile.pdf](@site/_dogfooding/_asset-tests/someFile.pdf)
- [@site/static/dogfooding/someFile.xyz](@site/static/dogfooding/someFile.xyz) - [@site/\_dogfooding/\_asset-tests/someFile.xyz](@site/_dogfooding/_asset-tests/someFile.xyz)
## Linking to non-SPA page hosted within website ## Linking to non-SPA page hosted within website

View file

@ -203,8 +203,8 @@ Code tag + double pipe: <code>||</code>
## Images edge cases ## Images edge cases
![](/dogfooding/新控制器空间/图片.png) ![](/新控制器空间/图片.png)
![](/dogfooding/4/图片.png) ![](/4/图片.png)
![](/dogfooding/4/docu.png) ![](/4/docu.png)

View file

@ -361,6 +361,20 @@ Attempting to add unknown field in the config will lead to error in build time:
Error: The field(s) 'foo', 'bar' are not recognized in docusaurus.config.js Error: The field(s) 'foo', 'bar' are not recognized in docusaurus.config.js
``` ```
### `staticDirectories` {#staticdirectories}
An array of paths, relative to the site's directory or absolute. Files under these paths will be copied to the build output as-is.
- Type: `string[]`
Example:
```js title="docusaurus.config.js"
module.exports = {
staticDirectories: ['static'],
};
```
### `scripts` {#scripts} ### `scripts` {#scripts}
An array of scripts to load. The values can be either strings or plain objects of attribute-value maps. The `<script>` tags will be inserted in the HTML `<head>`. An array of scripts to load. The values can be either strings or plain objects of attribute-value maps. The `<script>` tags will be inserted in the HTML `<head>`.

View file

@ -5,9 +5,7 @@ description: Handling assets in Docusaurus Markdown
slug: /markdown-features/assets slug: /markdown-features/assets
--- ---
Sometimes you want to link to static assets directly from Markdown files, and it is convenient to co-locate the asset next to the Markdown file using it. Sometimes you want to link to assets (e.g. docx files, images...) directly from Markdown files, and it is convenient to co-locate the asset next to the Markdown file using it.
We have setup Webpack loaders to handle most common file types, so that when you import a file, you get its url, and the asset is automatically copied to the output folder.
Let's imagine the following file structure: Let's imagine the following file structure:
@ -145,3 +143,16 @@ import ThemedImage from '@theme/ThemedImage';
}} }}
/> />
``` ```
## Static assets {#static-assets}
If a Markdown link or image has an absolute path, the path will be seen as a file path and will be resolved from the static directories. For example, if you have configured [static directories](../../static-assets.md) to be `['public', 'static']`, then for the following image:
```md title="my-doc.md"
![An image from the static](/img/docusaurus.png)
```
Docusaurus will try to look for it in both `static/img/docusaurus.png` and `public/img/docusaurus.png`. The link will then be converted to a `require` call instead of staying as a URL. This is desirable in two regards:
1. You don't have to worry about base URL, which Docusaurus will take care of when serving the asset;
2. The image enters Webpack's build pipeline and its name will be appended by a hash, which enables browsers to aggressively cache the image and improves your site's performance.

View file

@ -3,7 +3,7 @@ id: static-assets
title: Static Assets title: Static Assets
--- ---
Every website needs assets: images, stylesheets, favicons etc. In such cases, you can create a directory named `static` at the root of your project. Every website needs assets: images, stylesheets, favicons etc. By default, you are suggested to put these assets in the `static` folder.
Every file you put into **that directory will be copied** into the root of the generated `build` folder with the directory hierarchy preserved. E.g. if you add a file named `sun.jpg` to the static folder, it will be copied to `build/sun.jpg`. Every file you put into **that directory will be copied** into the root of the generated `build` folder with the directory hierarchy preserved. E.g. if you add a file named `sun.jpg` to the static folder, it will be copied to `build/sun.jpg`.
@ -12,13 +12,31 @@ This means that:
- for site `baseUrl: '/'`, the image `/static/img/docusaurus.png` will be served at `/img/docusaurus.png`. - for site `baseUrl: '/'`, the image `/static/img/docusaurus.png` will be served at `/img/docusaurus.png`.
- for site `baseUrl: '/subpath/'`, the image `/static/img/docusaurus.png` will be served at `/subpath/img/docusaurus.png`. - for site `baseUrl: '/subpath/'`, the image `/static/img/docusaurus.png` will be served at `/subpath/img/docusaurus.png`.
You can customize the static directory sources in `docusaurus.config.js`. For example, we can add `public` as another possible path:
```js title="docusaurus.config.js"
module.exports = {
title: 'My site',
staticDirectories: ['public', 'static'],
// ...
};
```
Now, all files in `public` as well as `static` will be copied to the build output.
## Referencing your static asset {#referencing-your-static-asset} ## Referencing your static asset {#referencing-your-static-asset}
You can reference assets from the `static` folder in your code using absolute paths, but this is not ideal because changing the site `baseUrl` will **break those link**s. In JSX, you can reference assets from the `static` folder in your code using absolute paths, but this is not ideal because changing the site `baseUrl` will **break those links**. For the image `<img src="/img/docusaurus.png" />` served at `https://example.com/test`, the browser will try to resolve it from the URL root, i.e. as `https://example.com/img/docusaurus.png`, which will fail because it's actually served at `https://example.com/test/img/docusaurus.png`.
You can `import` / `require()` the static asset (recommended), or use the `useBaseUrl` utility function: both prepend the `baseUrl` to paths for you. You can `import` / `require()` the static asset (recommended), or use the `useBaseUrl` utility function: both prepend the `baseUrl` to paths for you.
### JSX example {#jsx-example} :::info
In Markdown, things are different: you can stick to use absolute paths because Docusaurus actually handles them as `require` calls instead of URLs when parsing the Markdown. See [Markdown static assets](./guides/markdown-features/markdown-features-assets.mdx).
:::
### Examples {#examples}
```jsx title="MyComponent.js" ```jsx title="MyComponent.js"
import DocusaurusImageUrl from '@site/static/img/docusaurus.png'; import DocusaurusImageUrl from '@site/static/img/docusaurus.png';
@ -44,30 +62,6 @@ import DocusaurusLogoWithKeytar from '@site/static/img/docusaurus_keytar.svg';
<DocusaurusLogoWithKeytar title="Docusaurus Logo" className="logo" />; <DocusaurusLogoWithKeytar title="Docusaurus Logo" className="logo" />;
``` ```
### Markdown example {#markdown-example}
Markdown links and images referencing assets of the static folder will be converted to `require("@site/static/assetName.png")"`, and **the site baseUrl will be automatically prepended** for you.
```md title="my-doc.md"
![Docusaurus](/img/docusaurus.png)
```
Thanks to MDX, you can also use `useBaseUrl` utility function in Markdown files! You'd have to use html tags like `<img>` instead of the Markdown image syntax though. The syntax is exactly the same as in JSX.
```jsx title="my-doc.mdx"
---
id: my-doc
title: My Doc
---
// Add to the top of the file below the front matter.
import useBaseUrl from '@docusaurus/useBaseUrl';
...
<img alt="Docusaurus with Keytar" src={useBaseUrl('/img/docusaurus_keytar.svg')} />
```
### Caveats {#caveats} ### Caveats {#caveats}
Keep in mind that: Keep in mind that:

View file

@ -112,6 +112,7 @@ const config = {
description: description:
'An optimized site generator in React. Docusaurus helps you to move fast and write content. Build documentation websites, blogs, marketing pages, and more.', 'An optimized site generator in React. Docusaurus helps you to move fast and write content. Build documentation websites, blogs, marketing pages, and more.',
}, },
staticDirectories: ['static', path.join(__dirname, '_dogfooding/_asset-tests')],
clientModules: [require.resolve('./_dogfooding/clientModuleExample.ts')], clientModules: [require.resolve('./_dogfooding/clientModuleExample.ts')],
themes: ['@docusaurus/theme-live-codeblock'], themes: ['@docusaurus/theme-live-codeblock'],
plugins: [ plugins: [