Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Content] Throw on relative image usage #5648

Merged
merged 10 commits into from Dec 22, 2022
Merged
7 changes: 7 additions & 0 deletions .changeset/khaki-crabs-develop.md
@@ -0,0 +1,7 @@
---
'astro': patch
'@astrojs/mdx': patch
'@astrojs/markdown-remark': patch
---

Prevent relative image paths in `src/content/`
1 change: 1 addition & 0 deletions packages/astro/src/content/index.ts
Expand Up @@ -4,3 +4,4 @@ export {
} from './vite-plugin-content-assets.js';
export { astroContentServerPlugin } from './vite-plugin-content-server.js';
export { astroContentVirtualModPlugin } from './vite-plugin-content-virtual-mod.js';
export { getContentPaths } from './utils.js';
3 changes: 3 additions & 0 deletions packages/astro/src/core/build/generate.ts
Expand Up @@ -12,6 +12,7 @@ import type {
RouteType,
SSRLoadedRenderer,
} from '../../@types/astro';
import { getContentPaths } from '../../content/index.js';
import { BuildInternals, hasPrerenderedPages } from '../../core/build/internal.js';
import {
prependForwardSlash,
Expand Down Expand Up @@ -352,6 +353,8 @@ async function generatePath(
markdown: {
...settings.config.markdown,
isAstroFlavoredMd: settings.config.legacy.astroFlavoredMarkdown,
isExperimentalContentCollections: settings.config.experimental.contentCollections,
contentDir: getContentPaths(settings.config).contentDir,
},
mode: opts.mode,
renderers,
Expand Down
3 changes: 3 additions & 0 deletions packages/astro/src/core/build/vite-plugin-ssr.ts
Expand Up @@ -15,6 +15,7 @@ import { serializeRouteData } from '../routing/index.js';
import { addRollupInput } from './add-rollup-input.js';
import { getOutFile, getOutFolder } from './common.js';
import { eachPrerenderedPageData, eachServerPageData, sortedCSS } from './internal.js';
import { getContentPaths } from '../../content/index.js';

export const virtualModuleId = '@astrojs-ssr-virtual-entry';
const resolvedVirtualModuleId = '\0' + virtualModuleId;
Expand Down Expand Up @@ -208,6 +209,8 @@ function buildManifest(
markdown: {
...settings.config.markdown,
isAstroFlavoredMd: settings.config.legacy.astroFlavoredMarkdown,
isExperimentalContentCollections: settings.config.experimental.contentCollections,
contentDir: getContentPaths(settings.config).contentDir,
},
pageMap: null as any,
renderers: [],
Expand Down
3 changes: 3 additions & 0 deletions packages/astro/src/core/render/dev/environment.ts
@@ -1,4 +1,5 @@
import type { AstroSettings, RuntimeMode } from '../../../@types/astro';
import { getContentPaths } from '../../../content/index.js';
import type { LogOptions } from '../../logger/core.js';
import type { ModuleLoader } from '../../module-loader/index';
import type { Environment } from '../index';
Expand All @@ -24,6 +25,8 @@ export function createDevelopmentEnvironment(
markdown: {
...settings.config.markdown,
isAstroFlavoredMd: settings.config.legacy.astroFlavoredMarkdown,
isExperimentalContentCollections: settings.config.experimental.contentCollections,
contentDir: getContentPaths(settings.config).contentDir,
},
mode,
// This will be overridden in the dev server
Expand Down
6 changes: 5 additions & 1 deletion packages/astro/src/core/render/environment.ts
Expand Up @@ -37,7 +37,11 @@ export function createBasicEnvironment(options: CreateBasicEnvironmentArgs): Env
const mode = options.mode ?? 'development';
return createEnvironment({
...options,
markdown: options.markdown ?? {},
markdown: {
...(options.markdown ?? {}),
// Stub out, not important for basic rendering
contentDir: new URL('file:///src/content/'),
},
mode,
renderers: options.renderers ?? [],
resolve: options.resolve ?? ((s: string) => Promise.resolve(s)),
Expand Down
3 changes: 3 additions & 0 deletions packages/astro/src/vite-plugin-markdown-legacy/index.ts
Expand Up @@ -4,6 +4,7 @@ import matter from 'gray-matter';
import { fileURLToPath } from 'url';
import { Plugin, ResolvedConfig, transformWithEsbuild } from 'vite';
import type { AstroSettings } from '../@types/astro';
import { getContentPaths } from '../content/index.js';
import { pagesVirtualModuleId } from '../core/app/index.js';
import { cachedCompilation, CompileProps } from '../core/compile/index.js';
import { AstroErrorData, MarkdownError } from '../core/errors/index.js';
Expand Down Expand Up @@ -162,6 +163,8 @@ export default function markdown({ settings }: AstroPluginOptions): Plugin {
...renderOpts,
fileURL: fileUrl,
isAstroFlavoredMd: true,
isExperimentalContentCollections: settings.config.experimental.contentCollections,
contentDir: getContentPaths(settings.config).contentDir,
} as any);
let { code: astroResult, metadata } = renderResult;
const { layout = '', components = '', setup = '', ...content } = frontmatter;
Expand Down
3 changes: 3 additions & 0 deletions packages/astro/src/vite-plugin-markdown/index.ts
Expand Up @@ -5,6 +5,7 @@ import { fileURLToPath } from 'node:url';
import type { Plugin } from 'vite';
import { normalizePath } from 'vite';
import type { AstroSettings } from '../@types/astro';
import { getContentPaths } from '../content/index.js';
import { AstroErrorData, MarkdownError } from '../core/errors/index.js';
import type { LogOptions } from '../core/logger/core.js';
import { warn } from '../core/logger/core.js';
Expand Down Expand Up @@ -71,6 +72,8 @@ export default function markdown({ settings, logging }: AstroPluginOptions): Plu
...settings.config.markdown,
fileURL: new URL(`file://${fileId}`),
isAstroFlavoredMd: false,
isExperimentalContentCollections: settings.config.experimental.contentCollections,
contentDir: getContentPaths(settings.config).contentDir,
} as any);

const html = renderResult.code;
Expand Down
1 change: 1 addition & 0 deletions packages/integrations/mdx/package.json
Expand Up @@ -52,6 +52,7 @@
"@types/chai": "^4.3.1",
"@types/estree": "^1.0.0",
"@types/github-slugger": "^1.3.0",
"@types/mdast": "^3.0.10",
"@types/mocha": "^9.1.1",
"@types/yargs-parser": "^21.0.0",
"astro": "workspace:*",
Expand Down
38 changes: 37 additions & 1 deletion packages/integrations/mdx/src/plugins.ts
@@ -1,9 +1,11 @@
import { rehypeHeadingIds } from '@astrojs/markdown-remark';
import type { Image } from 'mdast';
import { nodeTypes } from '@mdx-js/mdx';
import type { PluggableList } from '@mdx-js/mdx/lib/core.js';
import type { Options as MdxRollupPluginOptions } from '@mdx-js/rollup';
import type { AstroConfig, MarkdownAstroData } from 'astro';
import type { Literal, MemberExpression } from 'estree';
import { visit } from 'unist-util-visit';
import { visit as estreeVisit } from 'estree-util-visit';
import { bold, yellow } from 'kleur/colors';
import rehypeRaw from 'rehype-raw';
Expand All @@ -15,7 +17,8 @@ import { rehypeInjectHeadingsExport } from './rehype-collect-headings.js';
import rehypeMetaString from './rehype-meta-string.js';
import remarkPrism from './remark-prism.js';
import remarkShiki from './remark-shiki.js';
import { jsToTreeNode } from './utils.js';
import { jsToTreeNode, isRelativePath } from './utils.js';
import { pathToFileURL } from 'node:url';

export function recmaInjectImportMetaEnvPlugin({
importMetaEnv,
Expand Down Expand Up @@ -113,6 +116,34 @@ export function rehypeApplyFrontmatterExport(pageFrontmatter: Record<string, any
};
}

/**
* `src/content/` does not support relative image paths.
* This plugin throws an error if any are found
*/
function toRemarkContentRelImageError({ srcDir }: { srcDir: URL }) {
const contentDir = new URL('content/', srcDir);
return function remarkContentRelImageError() {
return (tree: any, vfile: VFile) => {
const isContentFile = pathToFileURL(vfile.path).href.startsWith(contentDir.href);
if (!isContentFile) return;

const relImagePaths = new Set<string>();
visit(tree, 'image', function raiseError(node: Image) {
if (isRelativePath(node.url)) {
relImagePaths.add(node.url);
}
});
if (relImagePaths.size === 0) return;

const errorMessage =
`Relative image paths are not supported in the content/ directory. Place local images in the public/ directory and use absolute paths (see https://docs.astro.build/en/guides/images/#in-markdown-files):\n` +
[...relImagePaths].map((path) => JSON.stringify(path)).join(',\n');

throw new Error(errorMessage);
};
};
}

const DEFAULT_REMARK_PLUGINS: PluggableList = [remarkGfm, remarkSmartypants];
const DEFAULT_REHYPE_PLUGINS: PluggableList = [];

Expand Down Expand Up @@ -146,6 +177,11 @@ export async function getRemarkPlugins(
}

remarkPlugins = [...remarkPlugins, ...(mdxOptions.remarkPlugins ?? [])];

// Apply last in case user plugins resolve relative image paths
if (config.experimental.contentCollections) {
remarkPlugins.push(toRemarkContentRelImageError(config));
}
return remarkPlugins;
}

Expand Down
19 changes: 19 additions & 0 deletions packages/integrations/mdx/src/utils.ts
Expand Up @@ -95,3 +95,22 @@ export function handleExtendsNotSupported(pluginConfig: any) {
);
}
}

// Following utils taken from `packages/astro/src/core/path.ts`:

export function isRelativePath(path: string) {
return startsWithDotDotSlash(path) || startsWithDotSlash(path);
}

function startsWithDotDotSlash(path: string) {
const c1 = path[0];
const c2 = path[1];
const c3 = path[2];
return c1 === '.' && c2 === '.' && c3 === '/';
}

function startsWithDotSlash(path: string) {
const c1 = path[0];
const c2 = path[1];
return c1 === '.' && c2 === '/';
}
8 changes: 8 additions & 0 deletions packages/markdown/remark/src/index.ts
Expand Up @@ -14,6 +14,7 @@ import remarkPrism from './remark-prism.js';
import scopedStyles from './remark-scoped-styles.js';
import remarkShiki from './remark-shiki.js';
import remarkUnwrap from './remark-unwrap.js';
import toRemarkContentRelImageError from './remark-content-rel-image-error.js';

import rehypeRaw from 'rehype-raw';
import rehypeStringify from 'rehype-stringify';
Expand Down Expand Up @@ -42,6 +43,8 @@ export async function renderMarkdown(
remarkRehype = {},
extendDefaultPlugins = false,
isAstroFlavoredMd = false,
isExperimentalContentCollections = false,
contentDir,
} = opts;
const input = new VFile({ value: content, path: fileURL });
const scopedClassName = opts.$?.scopedClassName;
Expand Down Expand Up @@ -73,6 +76,11 @@ export async function renderMarkdown(
parser.use([remarkPrism(scopedClassName)]);
}

// Apply later in case user plugins resolve relative image paths
if (isExperimentalContentCollections) {
parser.use([toRemarkContentRelImageError({ contentDir })]);
}

parser.use([
[
markdownToHtml as any,
Expand Down
52 changes: 52 additions & 0 deletions packages/markdown/remark/src/remark-content-rel-image-error.ts
@@ -0,0 +1,52 @@
import type { Image } from 'mdast';
import { visit } from 'unist-util-visit';
import { pathToFileURL } from 'url';
import type { VFile } from 'vfile';

/**
* `src/content/` does not support relative image paths.
* This plugin throws an error if any are found
*/
export default function toRemarkContentRelImageError({ contentDir }: { contentDir: URL }) {
return function remarkContentRelImageError() {
return (tree: any, vfile: VFile) => {
const isContentFile = pathToFileURL(vfile.path).href.startsWith(contentDir.href);
if (!isContentFile) return;

const relImagePaths = new Set<string>();
visit(tree, 'image', function raiseError(node: Image) {
console.log(node.url);
if (isRelativePath(node.url)) {
relImagePaths.add(node.url);
}
});
if (relImagePaths.size === 0) return;

const errorMessage =
`Relative image paths are not supported in the content/ directory. Place local images in the public/ directory and use absolute paths (see https://docs.astro.build/en/guides/images/#in-markdown-files)\n` +
[...relImagePaths].map((path) => JSON.stringify(path)).join(',\n');

// Throw raw string to use `astro:markdown` default formatting
throw errorMessage;
};
};
}

// Following utils taken from `packages/astro/src/core/path.ts`:

function isRelativePath(path: string) {
return startsWithDotDotSlash(path) || startsWithDotSlash(path);
}

function startsWithDotDotSlash(path: string) {
const c1 = path[0];
const c2 = path[1];
const c3 = path[2];
return c1 === '.' && c2 === '.' && c3 === '/';
bholmesdev marked this conversation as resolved.
Show resolved Hide resolved
}

function startsWithDotSlash(path: string) {
const c1 = path[0];
const c2 = path[1];
return c1 === '.' && c2 === '/';
}
4 changes: 4 additions & 0 deletions packages/markdown/remark/src/types.ts
Expand Up @@ -54,6 +54,10 @@ export interface MarkdownRenderingOptions extends AstroMarkdownOptions {
scopedClassName: string | null;
};
isAstroFlavoredMd?: boolean;
/** Used to prevent relative image imports from `src/content/` */
isExperimentalContentCollections?: boolean;
/** Used to prevent relative image imports from `src/content/` */
contentDir: URL;
}

export interface MarkdownHeading {
Expand Down
2 changes: 2 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.