From cfcf2e2ffdaa68ace5c84329c05b83559a29d638 Mon Sep 17 00:00:00 2001 From: Ben Holmes Date: Fri, 24 Mar 2023 07:58:56 -0400 Subject: [PATCH] [Markdoc] Support automatic image optimization with `experimental.assets` (#6630) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * wip: scrappy implementation. It works! 🥳 * chore: add code comments on inline utils * fix: code cleanup, run on experimental.assets * feat: support ~/assets alias * fix: spoof `astro:assets` when outside experimental * test: image paths in dev and prod * feat: support any vite alias with ctx.resolve * fix: avoid trying to process absolute paths * fix: raise helpful error for invalid vite paths * refactor: revert URL support on emitAsset * chore: lint * refactor: expose emitESMImage from assets base * wip: why doesn't assets exist * scary chore: make @astrojs/markdoc truly depend on astro * fix: import emitESMImage straight from dist * chore: remove type def from assets package * chore: screw it, just ts ignore * deps: rollup types * refactor: optimize images during parse step * chore: remove unneeded `.flat()` * fix: use file-based relative paths * fix: add back helpful error * chore: changeset * deps: move astro back to dev dep * fix: put emit assets behind flag * chore: change to markdoc patch --- .changeset/big-rice-rest.md | 13 ++ packages/astro/src/@types/astro.ts | 9 +- packages/astro/src/assets/index.ts | 1 + packages/astro/src/assets/utils/emitAsset.ts | 34 +++- .../content/vite-plugin-content-imports.ts | 2 +- .../markdoc/components/TreeNode.ts | 10 +- packages/integrations/markdoc/package.json | 6 +- packages/integrations/markdoc/src/index.ts | 157 ++++++++++++++++-- packages/integrations/markdoc/src/utils.ts | 9 + .../fixtures/image-assets/astro.config.mjs | 10 ++ .../test/fixtures/image-assets/package.json | 9 + .../src/assets/alias/cityscape.jpg | Bin 0 -> 21402 bytes .../image-assets/src/assets/relative/oar.jpg | Bin 0 -> 17569 bytes .../image-assets/src/content/docs/intro.mdoc | 7 + .../image-assets/src/pages/index.astro | 19 +++ .../image-assets/src/public/favicon.svg | 9 + .../markdoc/test/image-assets.test.js | 76 +++++++++ pnpm-lock.yaml | 18 ++ 18 files changed, 368 insertions(+), 21 deletions(-) create mode 100644 .changeset/big-rice-rest.md create mode 100644 packages/integrations/markdoc/test/fixtures/image-assets/astro.config.mjs create mode 100644 packages/integrations/markdoc/test/fixtures/image-assets/package.json create mode 100644 packages/integrations/markdoc/test/fixtures/image-assets/src/assets/alias/cityscape.jpg create mode 100644 packages/integrations/markdoc/test/fixtures/image-assets/src/assets/relative/oar.jpg create mode 100644 packages/integrations/markdoc/test/fixtures/image-assets/src/content/docs/intro.mdoc create mode 100644 packages/integrations/markdoc/test/fixtures/image-assets/src/pages/index.astro create mode 100644 packages/integrations/markdoc/test/fixtures/image-assets/src/public/favicon.svg create mode 100644 packages/integrations/markdoc/test/image-assets.test.js diff --git a/.changeset/big-rice-rest.md b/.changeset/big-rice-rest.md new file mode 100644 index 0000000000..2b7efbaa5d --- /dev/null +++ b/.changeset/big-rice-rest.md @@ -0,0 +1,13 @@ +--- +'@astrojs/markdoc': patch +'astro': patch +--- + +Support automatic image optimization for Markdoc images when using `experimental.assets`. You can [follow our Assets guide](https://docs.astro.build/en/guides/assets/#enabling-assets-in-your-project) to enable this feature in your project. Then, start using relative or aliased image sources in your Markdoc files for automatic optimization: + +```md + +![The Milky Way Galaxy](../assets/galaxy.jpg) + +![Houston smiling and looking cute](~/assets/houston-smiling.jpg) +``` diff --git a/packages/astro/src/@types/astro.ts b/packages/astro/src/@types/astro.ts index 0af0842171..4eca60b344 100644 --- a/packages/astro/src/@types/astro.ts +++ b/packages/astro/src/@types/astro.ts @@ -1053,9 +1053,12 @@ export interface ContentEntryType { fileUrl: URL; contents: string; }): GetEntryInfoReturnType | Promise; - getRenderModule?(params: { - entry: ContentEntryModule; - }): rollup.LoadResult | Promise; + getRenderModule?( + this: rollup.PluginContext, + params: { + entry: ContentEntryModule; + } + ): rollup.LoadResult | Promise; contentModuleTypes?: string; } diff --git a/packages/astro/src/assets/index.ts b/packages/astro/src/assets/index.ts index f768c58dd0..04dde59797 100644 --- a/packages/astro/src/assets/index.ts +++ b/packages/astro/src/assets/index.ts @@ -2,3 +2,4 @@ export { getConfiguredImageService, getImage } from './internal.js'; export { baseService } from './services/service.js'; export { type LocalImageProps, type RemoteImageProps } from './types.js'; export { imageMetadata } from './utils/metadata.js'; +export { emitESMImage } from './utils/emitAsset.js'; diff --git a/packages/astro/src/assets/utils/emitAsset.ts b/packages/astro/src/assets/utils/emitAsset.ts index 74b851eed6..d6b34d9aa5 100644 --- a/packages/astro/src/assets/utils/emitAsset.ts +++ b/packages/astro/src/assets/utils/emitAsset.ts @@ -1,15 +1,15 @@ import fs from 'node:fs'; import path from 'node:path'; -import { pathToFileURL } from 'node:url'; -import type { AstroSettings } from '../../@types/astro'; -import { rootRelativePath } from '../../core/util.js'; +import { fileURLToPath, pathToFileURL } from 'node:url'; +import slash from 'slash'; +import type { AstroSettings, AstroConfig } from '../../@types/astro'; import { imageMetadata } from './metadata.js'; export async function emitESMImage( id: string, watchMode: boolean, fileEmitter: any, - settings: AstroSettings + settings: Pick ) { const url = pathToFileURL(id); const meta = await imageMetadata(url); @@ -41,3 +41,29 @@ export async function emitESMImage( return meta; } + +/** + * Utilities inlined from `packages/astro/src/core/util.ts` + * Avoids ESM / CJS bundling failures when accessed from integrations + * due to Vite dependencies in core. + */ + +function rootRelativePath(config: Pick, url: URL) { + const basePath = fileURLToNormalizedPath(url); + const rootPath = fileURLToNormalizedPath(config.root); + return prependForwardSlash(basePath.slice(rootPath.length)); +} + +function prependForwardSlash(filePath: string) { + return filePath[0] === '/' ? filePath : '/' + filePath; +} + +function fileURLToNormalizedPath(filePath: URL): string { + // Uses `slash` package instead of Vite's `normalizePath` + // to avoid CJS bundling issues. + return slash(fileURLToPath(filePath) + filePath.search).replace(/\\/g, '/'); +} + +export function emoji(char: string, fallback: string) { + return process.platform !== 'win32' ? char : fallback; +} diff --git a/packages/astro/src/content/vite-plugin-content-imports.ts b/packages/astro/src/content/vite-plugin-content-imports.ts index b699ea73e9..7ad71b31e5 100644 --- a/packages/astro/src/content/vite-plugin-content-imports.ts +++ b/packages/astro/src/content/vite-plugin-content-imports.ts @@ -139,7 +139,7 @@ export const _internal = { }); } - return contentRenderer({ entry }); + return contentRenderer.bind(this)({ entry }); }, }); } diff --git a/packages/integrations/markdoc/components/TreeNode.ts b/packages/integrations/markdoc/components/TreeNode.ts index b9b4c5c4de..f46355d5c8 100644 --- a/packages/integrations/markdoc/components/TreeNode.ts +++ b/packages/integrations/markdoc/components/TreeNode.ts @@ -1,6 +1,8 @@ import type { AstroInstance } from 'astro'; import type { RenderableTreeNode } from '@markdoc/markdoc'; import { createComponent, renderComponent, render } from 'astro/runtime/server/index.js'; +// @ts-expect-error Cannot find module 'astro:markdoc-assets' or its corresponding type declarations +import { Image } from 'astro:markdoc-assets'; import Markdoc from '@markdoc/markdoc'; import { MarkdocError, isCapitalized } from '../dist/utils.js'; @@ -45,10 +47,16 @@ export const ComponentNode = createComponent({ propagation: 'none', }); +const builtInComponents: Record = { + Image, +}; + export function createTreeNode( node: RenderableTreeNode, - components: Record = {} + userComponents: Record = {} ): TreeNode { + const components = { ...userComponents, ...builtInComponents }; + if (typeof node === 'string' || typeof node === 'number') { return { type: 'text', content: String(node) }; } else if (node === null || typeof node !== 'object' || !Markdoc.Tag.isTag(node)) { diff --git a/packages/integrations/markdoc/package.json b/packages/integrations/markdoc/package.json index 12bc6bacd4..70c0eea999 100644 --- a/packages/integrations/markdoc/package.json +++ b/packages/integrations/markdoc/package.json @@ -35,16 +35,20 @@ "gray-matter": "^4.0.3", "zod": "^3.17.3" }, + "peerDependencies": { + "astro": "workspace:*" + }, "devDependencies": { + "astro": "workspace:*", "@types/chai": "^4.3.1", "@types/html-escaper": "^3.0.0", "@types/mocha": "^9.1.1", - "astro": "workspace:*", "astro-scripts": "workspace:*", "chai": "^4.3.6", "devalue": "^4.2.0", "linkedom": "^0.14.12", "mocha": "^9.2.2", + "rollup": "^3.20.1", "vite": "^4.0.3" }, "engines": { diff --git a/packages/integrations/markdoc/src/index.ts b/packages/integrations/markdoc/src/index.ts index 70d005ee57..1d3556db7a 100644 --- a/packages/integrations/markdoc/src/index.ts +++ b/packages/integrations/markdoc/src/index.ts @@ -1,9 +1,23 @@ -import type { Config } from '@markdoc/markdoc'; +import type { + Config as ReadonlyMarkdocConfig, + ConfigType as MarkdocConfig, + Node, +} from '@markdoc/markdoc'; import Markdoc from '@markdoc/markdoc'; import type { AstroConfig, AstroIntegration, ContentEntryType, HookParameters } from 'astro'; import fs from 'node:fs'; +import type * as rollup from 'rollup'; import { fileURLToPath } from 'node:url'; -import { getAstroConfigPath, MarkdocError, parseFrontmatter } from './utils.js'; +import { + getAstroConfigPath, + isValidUrl, + MarkdocError, + parseFrontmatter, + prependForwardSlash, +} from './utils.js'; +// @ts-expect-error Cannot find module 'astro/assets' or its corresponding type declarations. +import { emitESMImage } from 'astro/assets'; +import type { Plugin as VitePlugin } from 'vite'; type SetupHookParams = HookParameters<'astro:config:setup'> & { // `contentEntryType` is not a public API @@ -11,12 +25,24 @@ type SetupHookParams = HookParameters<'astro:config:setup'> & { addContentEntryType: (contentEntryType: ContentEntryType) => void; }; -export default function markdoc(markdocConfig: Config = {}): AstroIntegration { +export default function markdocIntegration( + userMarkdocConfig: ReadonlyMarkdocConfig = {} +): AstroIntegration { return { name: '@astrojs/markdoc', hooks: { 'astro:config:setup': async (params) => { - const { updateConfig, config, addContentEntryType } = params as SetupHookParams; + const { + updateConfig, + config: astroConfig, + addContentEntryType, + } = params as SetupHookParams; + + updateConfig({ + vite: { + plugins: [safeAssetsVirtualModulePlugin({ astroConfig })], + }, + }); function getEntryInfo({ fileUrl, contents }: { fileUrl: URL; contents: string }) { const parsed = parseFrontmatter(contents, fileURLToPath(fileUrl)); @@ -30,16 +56,44 @@ export default function markdoc(markdocConfig: Config = {}): AstroIntegration { addContentEntryType({ extensions: ['.mdoc'], getEntryInfo, - getRenderModule({ entry }) { - validateRenderProperties(markdocConfig, config); + async getRenderModule({ entry }) { + validateRenderProperties(userMarkdocConfig, astroConfig); const ast = Markdoc.parse(entry.body); - const content = Markdoc.transform(ast, { - ...markdocConfig, + const pluginContext = this; + const markdocConfig: MarkdocConfig = { + ...userMarkdocConfig, variables: { - ...markdocConfig.variables, + ...userMarkdocConfig.variables, entry, }, - }); + }; + + if (astroConfig.experimental?.assets) { + await emitOptimizedImages(ast.children, { + astroConfig, + pluginContext, + filePath: entry._internal.filePath, + }); + + markdocConfig.nodes ??= {}; + markdocConfig.nodes.image = { + ...Markdoc.nodes.image, + transform(node, config) { + const attributes = node.transformAttributes(config); + const children = node.transformChildren(config); + + if (node.type === 'image' && '__optimizedSrc' in node.attributes) { + const { __optimizedSrc, ...rest } = node.attributes; + return new Markdoc.Tag('Image', { ...rest, src: __optimizedSrc }, children); + } else { + return new Markdoc.Tag('img', attributes, children); + } + }, + }; + } + + const content = Markdoc.transform(ast, markdocConfig); + return { code: `import { jsx as h } from 'astro/jsx-runtime';\nimport { Renderer } from '@astrojs/markdoc/components';\nconst transformedContent = ${JSON.stringify( content @@ -56,7 +110,54 @@ export default function markdoc(markdocConfig: Config = {}): AstroIntegration { }; } -function validateRenderProperties(markdocConfig: Config, astroConfig: AstroConfig) { +/** + * Emits optimized images, and appends the generated `src` to each AST node + * via the `__optimizedSrc` attribute. + */ +async function emitOptimizedImages( + nodeChildren: Node[], + ctx: { + pluginContext: rollup.PluginContext; + filePath: string; + astroConfig: AstroConfig; + } +) { + for (const node of nodeChildren) { + if ( + node.type === 'image' && + typeof node.attributes.src === 'string' && + shouldOptimizeImage(node.attributes.src) + ) { + // Attempt to resolve source with Vite. + // This handles relative paths and configured aliases + const resolved = await ctx.pluginContext.resolve(node.attributes.src, ctx.filePath); + + if (resolved?.id && fs.existsSync(new URL(prependForwardSlash(resolved.id), 'file://'))) { + const src = await emitESMImage( + resolved.id, + ctx.pluginContext.meta.watchMode, + ctx.pluginContext.emitFile, + { config: ctx.astroConfig } + ); + node.attributes.__optimizedSrc = src; + } else { + throw new MarkdocError({ + message: `Could not resolve image ${JSON.stringify( + node.attributes.src + )} from ${JSON.stringify(ctx.filePath)}. Does the file exist?`, + }); + } + } + await emitOptimizedImages(node.children, ctx); + } +} + +function shouldOptimizeImage(src: string) { + // Optimize anything that is NOT external or an absolute path to `public/` + return !isValidUrl(src) && !src.startsWith('/'); +} + +function validateRenderProperties(markdocConfig: ReadonlyMarkdocConfig, astroConfig: AstroConfig) { const tags = markdocConfig.tags ?? {}; const nodes = markdocConfig.nodes ?? {}; @@ -105,3 +206,37 @@ function validateRenderProperty({ function isCapitalized(str: string) { return str.length > 0 && str[0] === str[0].toUpperCase(); } + +/** + * TODO: remove when `experimental.assets` is baselined. + * + * `astro:assets` will fail to resolve if the `experimental.assets` flag is not enabled. + * This ensures a fallback for the Markdoc renderer to safely import at the top level. + * @see ../components/TreeNode.ts + */ +function safeAssetsVirtualModulePlugin({ + astroConfig, +}: { + astroConfig: Pick; +}): VitePlugin { + const virtualModuleId = 'astro:markdoc-assets'; + const resolvedVirtualModuleId = '\0' + virtualModuleId; + + return { + name: 'astro:markdoc-safe-assets-virtual-module', + resolveId(id) { + if (id === virtualModuleId) { + return resolvedVirtualModuleId; + } + }, + load(id) { + if (id !== resolvedVirtualModuleId) return; + + if (astroConfig.experimental?.assets) { + return `export { Image } from 'astro:assets';`; + } else { + return `export const Image = () => { throw new Error('Cannot use the Image component without the \`experimental.assets\` flag.'); }`; + } + }, + }; +} diff --git a/packages/integrations/markdoc/src/utils.ts b/packages/integrations/markdoc/src/utils.ts index 275c711f0e..9d6e5af266 100644 --- a/packages/integrations/markdoc/src/utils.ts +++ b/packages/integrations/markdoc/src/utils.ts @@ -145,3 +145,12 @@ const componentsPropValidator = z.record( export function isCapitalized(str: string) { return str.length > 0 && str[0] === str[0].toUpperCase(); } + +export function isValidUrl(str: string): boolean { + try { + new URL(str); + return true; + } catch { + return false; + } +} diff --git a/packages/integrations/markdoc/test/fixtures/image-assets/astro.config.mjs b/packages/integrations/markdoc/test/fixtures/image-assets/astro.config.mjs new file mode 100644 index 0000000000..9e64af363b --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/image-assets/astro.config.mjs @@ -0,0 +1,10 @@ +import { defineConfig } from 'astro/config'; +import markdoc from '@astrojs/markdoc'; + +// https://astro.build/config +export default defineConfig({ + experimental: { + assets: true, + }, + integrations: [markdoc()], +}); diff --git a/packages/integrations/markdoc/test/fixtures/image-assets/package.json b/packages/integrations/markdoc/test/fixtures/image-assets/package.json new file mode 100644 index 0000000000..30df52c2fa --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/image-assets/package.json @@ -0,0 +1,9 @@ +{ + "name": "@test/image-assets", + "version": "0.0.0", + "private": true, + "dependencies": { + "@astrojs/markdoc": "workspace:*", + "astro": "workspace:*" + } +} diff --git a/packages/integrations/markdoc/test/fixtures/image-assets/src/assets/alias/cityscape.jpg b/packages/integrations/markdoc/test/fixtures/image-assets/src/assets/alias/cityscape.jpg new file mode 100644 index 0000000000000000000000000000000000000000..6130e2c1482044f6e19c57369d91b4a97cb28ff4 GIT binary patch literal 21402 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 + + + + + + Astro + + + + + diff --git a/packages/integrations/markdoc/test/fixtures/image-assets/src/public/favicon.svg b/packages/integrations/markdoc/test/fixtures/image-assets/src/public/favicon.svg new file mode 100644 index 0000000000..f157bd1c5e --- /dev/null +++ b/packages/integrations/markdoc/test/fixtures/image-assets/src/public/favicon.svg @@ -0,0 +1,9 @@ + + + + diff --git a/packages/integrations/markdoc/test/image-assets.test.js b/packages/integrations/markdoc/test/image-assets.test.js new file mode 100644 index 0000000000..313977934b --- /dev/null +++ b/packages/integrations/markdoc/test/image-assets.test.js @@ -0,0 +1,76 @@ +import { parseHTML } from 'linkedom'; +import { expect } from 'chai'; +import { loadFixture } from '../../../astro/test/test-utils.js'; + +const root = new URL('./fixtures/image-assets/', import.meta.url); + +describe('Markdoc - Image assets', () => { + let baseFixture; + + before(async () => { + baseFixture = await loadFixture({ + root, + }); + }); + + describe('dev', () => { + let devServer; + + before(async () => { + devServer = await baseFixture.startDevServer(); + }); + + after(async () => { + await devServer.stop(); + }); + + it('uses public/ image paths unchanged', async () => { + const res = await baseFixture.fetch('/'); + const html = await res.text(); + const { document } = parseHTML(html); + expect(document.querySelector('#public > img')?.src).to.equal('/favicon.svg'); + }); + + it('transforms relative image paths to optimized path', async () => { + const res = await baseFixture.fetch('/'); + const html = await res.text(); + const { document } = parseHTML(html); + expect(document.querySelector('#relative > img')?.src).to.equal( + '/_image?href=%2Fsrc%2Fassets%2Frelative%2Foar.jpg%3ForigWidth%3D420%26origHeight%3D630%26origFormat%3Djpg&f=webp' + ); + }); + + it('transforms aliased image paths to optimized path', async () => { + const res = await baseFixture.fetch('/'); + const html = await res.text(); + const { document } = parseHTML(html); + expect(document.querySelector('#alias > img')?.src).to.equal( + '/_image?href=%2Fsrc%2Fassets%2Falias%2Fcityscape.jpg%3ForigWidth%3D420%26origHeight%3D280%26origFormat%3Djpg&f=webp' + ); + }); + }); + + describe('build', () => { + before(async () => { + await baseFixture.build(); + }); + + it('uses public/ image paths unchanged', async () => { + const html = await baseFixture.readFile('/index.html'); + const { document } = parseHTML(html); + expect(document.querySelector('#public > img')?.src).to.equal('/favicon.svg'); + }); + + it('transforms relative image paths to optimized path', async () => { + const html = await baseFixture.readFile('/index.html'); + const { document } = parseHTML(html); + expect(document.querySelector('#relative > img')?.src).to.match(/^\/_astro\/oar.*\.webp$/); + }); + + it('transforms aliased image paths to optimized path', async () => { + const html = await baseFixture.readFile('/index.html'); + const { document } = parseHTML(html); + expect(document.querySelector('#alias > img')?.src).to.match(/^\/_astro\/cityscape.*\.webp$/); + }); + }); +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 37491d8c70..441fe16a75 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3080,6 +3080,7 @@ importers: gray-matter: ^4.0.3 linkedom: ^0.14.12 mocha: ^9.2.2 + rollup: ^3.20.1 vite: ^4.0.3 zod: ^3.17.3 dependencies: @@ -3096,6 +3097,7 @@ importers: devalue: 4.2.3 linkedom: 0.14.21 mocha: 9.2.2 + rollup: 3.20.1 vite: 4.1.2 packages/integrations/markdoc/test/fixtures/content-collections: @@ -3119,6 +3121,14 @@ importers: '@astrojs/markdoc': link:../../.. astro: link:../../../../../astro + packages/integrations/markdoc/test/fixtures/image-assets: + specifiers: + '@astrojs/markdoc': workspace:* + astro: workspace:* + dependencies: + '@astrojs/markdoc': link:../../.. + astro: link:../../../../../astro + packages/integrations/mdx: specifiers: '@astrojs/markdown-remark': ^2.1.2 @@ -14921,6 +14931,14 @@ packages: optionalDependencies: fsevents: 2.3.2 + /rollup/3.20.1: + resolution: {integrity: sha512-sz2w8cBJlWQ2E17RcpvHuf4sk2BQx4tfKDnjNPikEpLEevrbIAR7CH3PGa2hpPwWbNgPaA9yh9Jzljds5bc9zg==} + engines: {node: '>=14.18.0', npm: '>=8.0.0'} + hasBin: true + optionalDependencies: + fsevents: 2.3.2 + dev: true + /run-parallel/1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} dependencies: