Skip to content

Commit

Permalink
Fix: component styles within imported markdown files (#3116)
Browse files Browse the repository at this point in the history
* fix: replace markdown path prefix with suffix flag

* fix: avoid non-encoded colons for flag

* fix: remove needless ?

* fix: dev server load order

* fix: production build crawl dynamic imports

* fix: remove  unused virtual_module_id const

* fix: remove unsafe "!" on getmodbyid

* fix: remove needless @id path check

* fix: add list of SSR-able file extensions

* docs: virtual_mod_id change

* fix: support id prefix on resolved ids

* fix: switch to ?mdImport flag to resolve glob imports

* tests: imported md styles for dev and build

* chore: changeset
  • Loading branch information
bholmesdev authored Apr 18, 2022
1 parent dfa1042 commit 44bacd2
Show file tree
Hide file tree
Showing 13 changed files with 162 additions and 19 deletions.
5 changes: 5 additions & 0 deletions .changeset/tidy-poems-occur.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'astro': patch
---

Fix: Astro components used in dynamically imported markdown (ex. Astro.glob('\*.md') will now retain their CSS styles in dev and production builds
27 changes: 23 additions & 4 deletions packages/astro/src/core/render/dev/css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,21 @@ import path from 'path';
import { unwrapId, viteID } from '../../util.js';
import { STYLE_EXTENSIONS } from '../util.js';

/**
* List of file extensions signalling we can (and should) SSR ahead-of-time
* See usage below
*/
const fileExtensionsToSSR = new Set(['.md']);

/** Given a filePath URL, crawl Vite’s module graph to find all style imports. */
export function getStylesForURL(filePath: URL, viteServer: vite.ViteDevServer): Set<string> {
export async function getStylesForURL(
filePath: URL,
viteServer: vite.ViteDevServer
): Promise<Set<string>> {
const importedCssUrls = new Set<string>();

/** recursively crawl the module graph to get all style files imported by parent id */
function crawlCSS(_id: string, isFile: boolean, scanned = new Set<string>()) {
async function crawlCSS(_id: string, isFile: boolean, scanned = new Set<string>()) {
const id = unwrapId(_id);
const importedModules = new Set<vite.ModuleNode>();
const moduleEntriesForId = isFile
Expand All @@ -32,6 +41,16 @@ export function getStylesForURL(filePath: URL, viteServer: vite.ViteDevServer):
if (id === entry.id) {
scanned.add(id);
for (const importedModule of entry.importedModules) {
// some dynamically imported modules are *not* server rendered in time
// to only SSR modules that we can safely transform, we check against
// a list of file extensions based on our built-in vite plugins
if (importedModule.id) {
// use URL to strip special query params like "?content"
const { pathname } = new URL(`file://${importedModule.id}`);
if (fileExtensionsToSSR.has(path.extname(pathname))) {
await viteServer.ssrLoadModule(importedModule.id);
}
}
importedModules.add(importedModule);
}
}
Expand All @@ -48,11 +67,11 @@ export function getStylesForURL(filePath: URL, viteServer: vite.ViteDevServer):
// NOTE: We use the `url` property here. `id` would break Windows.
importedCssUrls.add(importedModule.url);
}
crawlCSS(importedModule.id, false, scanned);
await crawlCSS(importedModule.id, false, scanned);
}
}

// Crawl your import graph for CSS files, populating `importedCssUrls` as a result.
crawlCSS(viteID(filePath), true);
await crawlCSS(viteID(filePath), true);
return importedCssUrls;
}
4 changes: 2 additions & 2 deletions packages/astro/src/core/render/dev/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ export async function render(
// Pass framework CSS in as link tags to be appended to the page.
let links = new Set<SSRElement>();
if (!isLegacyBuild) {
[...getStylesForURL(filePath, viteServer)].forEach((href) => {
[...(await getStylesForURL(filePath, viteServer))].forEach((href) => {
if (mode === 'development' && svelteStylesRE.test(href)) {
scripts.add({
props: { type: 'module', src: href },
Expand Down Expand Up @@ -211,7 +211,7 @@ export async function render(

// inject CSS
if (isLegacyBuild) {
[...getStylesForURL(filePath, viteServer)].forEach((href) => {
[...(await getStylesForURL(filePath, viteServer))].forEach((href) => {
if (mode === 'development' && svelteStylesRE.test(href)) {
tags.push({
tag: 'script',
Expand Down
2 changes: 1 addition & 1 deletion packages/astro/src/vite-plugin-build-css/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ export function rollupPluginAstroBuildCSS(options: PluginOptions): VitePlugin {

const info = ctx.getModuleInfo(id);
if (info) {
for (const importedId of info.importedIds) {
for (const importedId of [...info.importedIds, ...info.dynamicallyImportedIds]) {
if (!seen.has(importedId) && !isRawOrUrlModule(importedId)) {
yield* walkStyles(ctx, importedId, seen);
}
Expand Down
20 changes: 8 additions & 12 deletions packages/astro/src/vite-plugin-markdown/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ interface AstroPluginOptions {
config: AstroConfig;
}

const VIRTUAL_MODULE_ID_PREFIX = 'astro:markdown';
const VIRTUAL_MODULE_ID = '\0' + VIRTUAL_MODULE_ID_PREFIX;
const MARKDOWN_IMPORT_FLAG = '?mdImport';
const MARKDOWN_CONTENT_FLAG = '?content';

// TODO: Clean up some of the shared logic between this Markdown plugin and the Astro plugin.
// Both end up connecting a `load()` hook to the Astro compiler, and share some copy-paste
Expand Down Expand Up @@ -53,16 +53,12 @@ export default function markdown({ config }: AstroPluginOptions): Plugin {
name: 'astro:markdown',
enforce: 'pre',
async resolveId(id, importer, options) {
// Resolve virtual modules as-is.
if (id.startsWith(VIRTUAL_MODULE_ID)) {
return id;
}
// Resolve any .md files with the `?content` cache buster. This should only come from
// an already-resolved JS module wrapper. Needed to prevent infinite loops in Vite.
// Unclear if this is expected or if cache busting is just working around a Vite bug.
if (id.endsWith('.md?content')) {
if (id.endsWith(`.md${MARKDOWN_CONTENT_FLAG}`)) {
const resolvedId = await this.resolve(id, importer, { skipSelf: true, ...options });
return resolvedId?.id.replace('?content', '');
return resolvedId?.id.replace(MARKDOWN_CONTENT_FLAG, '');
}
// If the markdown file is imported from another file via ESM, resolve a JS representation
// that defers the markdown -> HTML rendering until it is needed. This is especially useful
Expand All @@ -71,7 +67,7 @@ export default function markdown({ config }: AstroPluginOptions): Plugin {
if (id.endsWith('.md') && !isRootImport(importer)) {
const resolvedId = await this.resolve(id, importer, { skipSelf: true, ...options });
if (resolvedId) {
return VIRTUAL_MODULE_ID + resolvedId.id;
return resolvedId.id + MARKDOWN_IMPORT_FLAG;
}
}
// In all other cases, we do nothing and rely on normal Vite resolution.
Expand All @@ -81,11 +77,11 @@ export default function markdown({ config }: AstroPluginOptions): Plugin {
// A markdown file has been imported via ESM!
// Return the file's JS representation, including all Markdown
// frontmatter and a deferred `import() of the compiled markdown content.
if (id.startsWith(VIRTUAL_MODULE_ID)) {
if (id.endsWith(`.md${MARKDOWN_IMPORT_FLAG}`)) {
const sitePathname = config.site
? appendForwardSlash(new URL(config.base, config.site).pathname)
: '/';
const fileId = id.substring(VIRTUAL_MODULE_ID.length);
const fileId = id.replace(MARKDOWN_IMPORT_FLAG, '');
const fileUrl = fileId.includes('/pages/')
? fileId.replace(/^.*\/pages\//, sitePathname).replace(/(\/index)?\.md$/, '')
: undefined;
Expand All @@ -100,7 +96,7 @@ export default function markdown({ config }: AstroPluginOptions): Plugin {
// Deferred
export default async function load() {
return (await import(${JSON.stringify(fileId + '?content')}));
return (await import(${JSON.stringify(fileId + MARKDOWN_CONTENT_FLAG)}));
};
export function Content(...args) {
return load().then((m) => m.default(...args))
Expand Down
59 changes: 59 additions & 0 deletions packages/astro/test/astro-markdown-css.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { expect } from 'chai';
import cheerio from 'cheerio';
import { loadFixture } from './test-utils.js';

let fixture;
const IMPORTED_ASTRO_COMPONENT_ID = 'imported-astro-component'

describe('Imported markdown CSS', function () {
before(async () => {
fixture = await loadFixture({ root: './fixtures/astro-markdown-css/' });
});
describe('build', () => {
let $;
let bundledCSS;

before(async () => {
this.timeout(45000); // test needs a little more time in CI
await fixture.build();

// get bundled CSS (will be hashed, hence DOM query)
const html = await fixture.readFile('/index.html');
$ = cheerio.load(html);
const bundledCSSHREF = $('link[rel=stylesheet][href^=/assets/]').attr('href');
bundledCSS = await fixture.readFile(bundledCSSHREF.replace(/^\/?/, '/'));
});

it('Compiles styles for Astro components within imported markdown', () => {
const importedAstroComponent = $(`#${IMPORTED_ASTRO_COMPONENT_ID}`)?.[0]
expect(importedAstroComponent?.name).to.equal('h2')
const cssClass = $(importedAstroComponent).attr('class')?.split(/\s+/)?.[0]

expect(bundledCSS).to.match(new RegExp(`h2.${cssClass}{color:#00f}`))
});
});
describe('dev', () => {
let devServer;
let $;

before(async () => {
devServer = await fixture.startDevServer();
const html = await fixture.fetch('/').then((res) => res.text());
$ = cheerio.load(html);
});

after(async () => {
await devServer.stop();
});

it('Compiles styles for Astro components within imported markdown', async () => {
const importedAstroComponent = $(`#${IMPORTED_ASTRO_COMPONENT_ID}`)?.[0]
expect(importedAstroComponent?.name).to.equal('h2')
const cssClass = $(importedAstroComponent).attr('class')?.split(/\s+/)?.[0]

const astroCSSHREF = $('link[rel=stylesheet][href^=/src/components/Visual.astro]').attr('href');
const css = await fixture.fetch(astroCSSHREF.replace(/^\/?/, '/')).then((res) => res.text());
expect(css).to.match(new RegExp(`h2.${cssClass}{color:#00f}`));
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { defineConfig } from 'astro/config';

// https://astro.build/config
export default defineConfig({
integrations: []
});
12 changes: 12 additions & 0 deletions packages/astro/test/fixtures/astro-markdown-css/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"name": "@test/astro-markdown-css",
"version": "0.0.0",
"private": true,
"scripts": {
"build": "astro build",
"dev": "astro dev"
},
"dependencies": {
"astro": "workspace:*"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<h2 id="imported-astro-component">I'm a visual!</h2>

<style>
h2 {
color: #00f;
}
</style>
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
setup: import Visual from '../components/Visual.astro'
---

# Example markdown document, with a Visual

<Visual />
<Visual />
<Visual />
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
setup: import Visual from '../components/Visual.astro'
---

# Example markdown document, with a more Visuals

<Visual />
<Visual />
<Visual />
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
const markdownDocs = await Astro.glob('../markdown/*.md')
const article2 = await import('../markdown/article2.md')
---
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width" />
<title>Astro</title>
</head>
<body>
{markdownDocs.map(markdownDoc => <><h2>{markdownDoc.url}</h2><markdownDoc.Content /></>)}
<article2.Content />
</body>
</html>
6 changes: 6 additions & 0 deletions pnpm-lock.yaml

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

0 comments on commit 44bacd2

Please sign in to comment.