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 & {
staticDir?: string;
staticDirs: string[];
siteDir: string;
isMDXPartial?: (filePath: string) => boolean;
isMDXPartialFrontMatterWarningDisabled?: boolean;
removeContentTitle?: boolean;
@ -123,8 +124,15 @@ export default async function mdxLoader(
remarkPlugins: [
...(reqOptions.beforeDefaultRemarkPlugins || []),
...DEFAULT_OPTIONS.remarkPlugins,
[transformImage, {staticDir: reqOptions.staticDir, filePath}],
[transformLinks, {staticDir: reqOptions.staticDir, filePath}],
[transformImage, {staticDirs: reqOptions.staticDirs, filePath}],
[
transformLinks,
{
staticDirs: reqOptions.staticDirs,
filePath,
siteDir: reqOptions.siteDir,
},
],
...(reqOptions.remarkPlugins || []),
],
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
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`] = `
"![img](/img/unchecked.png)
@ -12,20 +12,22 @@ exports[`transformImage plugin pathname protocol 1`] = `
exports[`transformImage plugin transform md images to <img /> 1`] = `
"![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
\`\`\`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.
*/
import {join, relative} from 'path';
import path from 'path';
import remark from 'remark';
import mdx from 'remark-mdx';
import vfile from 'to-vfile';
@ -13,47 +13,48 @@ import plugin from '../index';
import headings from '../../headings/index';
const processFixture = async (name, options) => {
const path = join(__dirname, 'fixtures', `${name}.md`);
const file = await vfile.read(path);
const filePath = path.join(__dirname, `__fixtures__/${name}.md`);
const file = await vfile.read(filePath);
const result = await remark()
.use(headings)
.use(mdx)
.use(plugin, {...options, filePath: path})
.use(plugin, {...options, filePath})
.process(file);
return result.toString();
};
// avoid hardcoding absolute
const staticDir = `./${relative(process.cwd(), join(__dirname, 'fixtures'))}`;
const staticDirs = [
// 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', () => {
test('fail if image does not exist', async () => {
await expect(
processFixture('fail', {
staticDir,
}),
processFixture('fail', {staticDirs}),
).rejects.toThrowErrorMatchingSnapshot();
});
test('fail if image url is absent', async () => {
await expect(
processFixture('noUrl', {
staticDir,
}),
processFixture('noUrl', {staticDirs}),
).rejects.toThrowErrorMatchingSnapshot();
});
test('transform md images to <img />', async () => {
const result = await processFixture('img', {
staticDir,
});
const result = await processFixture('img', {staticDirs});
expect(result).toMatchSnapshot();
});
test('pathname protocol', async () => {
const result = await processFixture('pathname', {
staticDir,
});
const result = await processFixture('pathname', {staticDirs});
expect(result).toMatchSnapshot();
});
});

View file

@ -25,7 +25,7 @@ const {
interface PluginOptions {
filePath: string;
staticDir: string;
staticDirs: 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(
node: Image,
{filePath, staticDir}: PluginOptions,
{filePath, staticDirs}: PluginOptions,
) {
if (!node.url) {
throw new Error(
@ -88,9 +104,11 @@ async function processImageNode(
// images without protocol
else if (path.isAbsolute(node.url)) {
// absolute paths are expected to exist in the static folder
const expectedImagePath = path.join(staticDir, node.url);
await ensureImageFileExist(expectedImagePath, filePath);
createJSX(node, posixPath(expectedImagePath));
const possibleImagePaths = staticDirs.map((dir) =>
path.join(dir, node.url),
);
const imagePath = await findImage(possibleImagePaths, filePath);
createJSX(node, posixPath(imagePath));
}
// We try to convert image urls without protocol to images with require calls
// going through webpack ensures that image assets exist at build time

View file

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

View file

@ -1,6 +1,6 @@
// 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`] = `
"[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]!./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}>@site/static/staticAsset.pdf</a>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -13,7 +13,6 @@ import {
DEFAULT_BUILD_DIR_NAME,
DEFAULT_CONFIG_FILE_NAME,
GENERATED_FILES_DIR_NAME,
STATIC_DIR_NAME,
} from '../constants';
import loadClientModules from './client-modules';
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
// 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
function createMDXFallbackPlugin({siteDir}: {siteDir: string}): LoadedPlugin {
function createMDXFallbackPlugin({
siteDir,
siteConfig,
}: {
siteDir: string;
siteConfig: DocusaurusConfig;
}): LoadedPlugin {
return {
name: 'docusaurus-mdx-fallback-plugin',
content: null,
@ -223,7 +228,10 @@ function createMDXFallbackPlugin({siteDir}: {siteDir: string}): LoadedPlugin {
{
loader: require.resolve('@docusaurus/mdx-loader'),
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
isMDXPartialFrontMatterWarningDisabled: true, // External mdx files might have frontmatter, let's just disable the warning
remarkPlugins: [admonitions],
@ -273,7 +281,7 @@ export async function load(
);
plugins.push(createBootstrapPlugin({siteConfig}));
plugins.push(createMDXFallbackPlugin({siteDir}));
plugins.push(createMDXFallbackPlugin({siteDir, siteConfig}));
// Load client modules.
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)
- [/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

View file

@ -203,8 +203,8 @@ Code tag + double pipe: <code>||</code>
## 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
```
### `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}
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
---
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.
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.
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.
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
---
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`.
@ -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: '/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}
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.
### 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"
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" />;
```
### 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}
Keep in mind that:

View file

@ -112,6 +112,7 @@ const config = {
description:
'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')],
themes: ['@docusaurus/theme-live-codeblock'],
plugins: [