summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGravatar Ben Holmes <hey@bholmes.dev> 2023-05-19 14:12:45 -0400
committerGravatar GitHub <noreply@github.com> 2023-05-19 14:12:45 -0400
commita9e1cd7e58794fe220539c2ed935c9eb96bab55a (patch)
tree07c298f7a21d61726f515dcae762fd0cbe277540
parent147373722b37126af949bb054a1cdfb0aed6c2ff (diff)
downloadastro-a9e1cd7e58794fe220539c2ed935c9eb96bab55a.tar.gz
astro-a9e1cd7e58794fe220539c2ed935c9eb96bab55a.tar.zst
astro-a9e1cd7e58794fe220539c2ed935c9eb96bab55a.zip
Fix: Heading ID CI flakiness (#7141)
* feat: use `ctx` object instead of leaky global * test: heading IDs stale caches * chore: changeset
-rw-r--r--.changeset/eleven-tables-speak.md5
-rw-r--r--packages/integrations/markdoc/src/index.ts12
-rw-r--r--packages/integrations/markdoc/src/nodes/heading.ts32
-rw-r--r--packages/integrations/markdoc/src/nodes/index.ts2
-rw-r--r--packages/integrations/markdoc/src/runtime.ts46
-rw-r--r--packages/integrations/markdoc/test/fixtures/headings-custom/src/content/docs/headings-stale-cache-check.mdoc13
-rw-r--r--packages/integrations/markdoc/test/fixtures/headings-custom/src/pages/[slug].astro (renamed from packages/integrations/markdoc/test/fixtures/headings-custom/src/pages/index.astro)12
-rw-r--r--packages/integrations/markdoc/test/fixtures/headings/src/content/docs/headings-stale-cache-check.mdoc13
-rw-r--r--packages/integrations/markdoc/test/fixtures/headings/src/pages/[slug].astro (renamed from packages/integrations/markdoc/test/fixtures/headings/src/pages/index.astro)12
-rw-r--r--packages/integrations/markdoc/test/headings.test.js50
10 files changed, 154 insertions, 43 deletions
diff --git a/.changeset/eleven-tables-speak.md b/.changeset/eleven-tables-speak.md
new file mode 100644
index 000000000..44aff3211
--- /dev/null
+++ b/.changeset/eleven-tables-speak.md
@@ -0,0 +1,5 @@
+---
+'@astrojs/markdoc': patch
+---
+
+Fix inconsistent Markdoc heading IDs for documents with the same headings.
diff --git a/packages/integrations/markdoc/src/index.ts b/packages/integrations/markdoc/src/index.ts
index 4360800a0..627f08c77 100644
--- a/packages/integrations/markdoc/src/index.ts
+++ b/packages/integrations/markdoc/src/index.ts
@@ -10,7 +10,7 @@ import { emitESMImage } from 'astro/assets';
import { bold, red, yellow } from 'kleur/colors';
import type * as rollup from 'rollup';
import { loadMarkdocConfig, type MarkdocConfigResult } from './load-config.js';
-import { applyDefaultConfig } from './runtime.js';
+import { setupConfig } from './runtime.js';
type SetupHookParams = HookParameters<'astro:config:setup'> & {
// `contentEntryType` is not a public API
@@ -52,7 +52,7 @@ export default function markdocIntegration(legacyConfig?: any): AstroIntegration
async getRenderModule({ entry, viteId }) {
const ast = Markdoc.parse(entry.body);
const pluginContext = this;
- const markdocConfig = applyDefaultConfig(userMarkdocConfig, entry);
+ const markdocConfig = setupConfig(userMarkdocConfig, entry);
const validationErrors = Markdoc.validate(ast, markdocConfig).filter((e) => {
return (
@@ -90,7 +90,7 @@ export default function markdocIntegration(legacyConfig?: any): AstroIntegration
const res = `import { jsx as h } from 'astro/jsx-runtime';
import { Renderer } from '@astrojs/markdoc/components';
- import { collectHeadings, applyDefaultConfig, Markdoc, headingSlugger } from '@astrojs/markdoc/runtime';
+ import { collectHeadings, setupConfig, Markdoc } from '@astrojs/markdoc/runtime';
import * as entry from ${JSON.stringify(viteId + '?astroContentCollectionEntry')};
${
markdocConfigResult
@@ -113,16 +113,14 @@ export function getHeadings() {
instead of the Content component. Would remove double-transform and unlock variable resolution in heading slugs. */
''
}
- headingSlugger.reset();
const headingConfig = userConfig.nodes?.heading;
- const config = applyDefaultConfig(headingConfig ? { nodes: { heading: headingConfig } } : {}, entry);
+ const config = setupConfig(headingConfig ? { nodes: { heading: headingConfig } } : {}, entry);
const ast = Markdoc.Ast.fromJSON(stringifiedAst);
const content = Markdoc.transform(ast, config);
return collectHeadings(Array.isArray(content) ? content : content.children);
}
export async function Content (props) {
- headingSlugger.reset();
- const config = applyDefaultConfig({
+ const config = setupConfig({
...userConfig,
variables: { ...userConfig.variables, ...props },
}, entry);
diff --git a/packages/integrations/markdoc/src/nodes/heading.ts b/packages/integrations/markdoc/src/nodes/heading.ts
index 8adf57612..19a988b63 100644
--- a/packages/integrations/markdoc/src/nodes/heading.ts
+++ b/packages/integrations/markdoc/src/nodes/heading.ts
@@ -1,10 +1,19 @@
-import Markdoc, { type RenderableTreeNode, type Schema } from '@markdoc/markdoc';
+import Markdoc, { type RenderableTreeNode, type Schema, type ConfigType } from '@markdoc/markdoc';
import Slugger from 'github-slugger';
import { getTextContent } from '../runtime.js';
-export const headingSlugger = new Slugger();
+type ConfigTypeWithCtx = ConfigType & {
+ // TODO: decide on `ctx` as a convention for config merging
+ ctx: {
+ headingSlugger: Slugger;
+ };
+};
-function getSlug(attributes: Record<string, any>, children: RenderableTreeNode[]): string {
+function getSlug(
+ attributes: Record<string, any>,
+ children: RenderableTreeNode[],
+ headingSlugger: Slugger
+): string {
if (attributes.id && typeof attributes.id === 'string') {
return attributes.id;
}
@@ -21,11 +30,11 @@ export const heading: Schema = {
id: { type: String },
level: { type: Number, required: true, default: 1 },
},
- transform(node, config) {
+ transform(node, config: ConfigTypeWithCtx) {
const { level, ...attributes } = node.transformAttributes(config);
const children = node.transformChildren(config);
- const slug = getSlug(attributes, children);
+ const slug = getSlug(attributes, children, config.ctx.headingSlugger);
const render = config.nodes?.heading?.render ?? `h${level}`;
const tagProps =
@@ -39,3 +48,16 @@ export const heading: Schema = {
return new Markdoc.Tag(render, tagProps, children);
},
};
+
+export function setupHeadingConfig(): ConfigTypeWithCtx {
+ const headingSlugger = new Slugger();
+
+ return {
+ ctx: {
+ headingSlugger,
+ },
+ nodes: {
+ heading,
+ },
+ };
+}
diff --git a/packages/integrations/markdoc/src/nodes/index.ts b/packages/integrations/markdoc/src/nodes/index.ts
index c25b03f27..4cd7e3667 100644
--- a/packages/integrations/markdoc/src/nodes/index.ts
+++ b/packages/integrations/markdoc/src/nodes/index.ts
@@ -1,4 +1,4 @@
import { heading } from './heading.js';
-export { headingSlugger } from './heading.js';
+export { setupHeadingConfig } from './heading.js';
export const nodes = { heading };
diff --git a/packages/integrations/markdoc/src/runtime.ts b/packages/integrations/markdoc/src/runtime.ts
index 61b38fd02..3164cda13 100644
--- a/packages/integrations/markdoc/src/runtime.ts
+++ b/packages/integrations/markdoc/src/runtime.ts
@@ -4,27 +4,45 @@ import Markdoc, {
type RenderableTreeNode,
} from '@markdoc/markdoc';
import type { ContentEntryModule } from 'astro';
-import { nodes as astroNodes } from './nodes/index.js';
+import { setupHeadingConfig } from './nodes/index.js';
-/** Used to reset Slugger cache on each build at runtime */
+/** Used to call `Markdoc.transform()` and `Markdoc.Ast` in runtime modules */
export { default as Markdoc } from '@markdoc/markdoc';
-export { headingSlugger } from './nodes/index.js';
-export function applyDefaultConfig(
- config: MarkdocConfig,
- entry: ContentEntryModule
-): MarkdocConfig {
+/**
+ * Merge user config with default config and set up context (ex. heading ID slugger)
+ * Called on each file's individual transform
+ */
+export function setupConfig(userConfig: MarkdocConfig, entry: ContentEntryModule): MarkdocConfig {
+ const defaultConfig: MarkdocConfig = {
+ // `setupXConfig()` could become a "plugin" convention as well?
+ ...setupHeadingConfig(),
+ variables: { entry },
+ };
+ return mergeConfig(defaultConfig, userConfig);
+}
+
+/** Merge function from `@markdoc/markdoc` internals */
+function mergeConfig(configA: MarkdocConfig, configB: MarkdocConfig): MarkdocConfig {
return {
- ...config,
- variables: {
- entry,
- ...config.variables,
+ ...configA,
+ ...configB,
+ tags: {
+ ...configA.tags,
+ ...configB.tags,
},
nodes: {
- ...astroNodes,
- ...config.nodes,
+ ...configA.nodes,
+ ...configB.nodes,
+ },
+ functions: {
+ ...configA.functions,
+ ...configB.functions,
+ },
+ variables: {
+ ...configA.variables,
+ ...configB.variables,
},
- // TODO: Syntax highlighting
};
}
diff --git a/packages/integrations/markdoc/test/fixtures/headings-custom/src/content/docs/headings-stale-cache-check.mdoc b/packages/integrations/markdoc/test/fixtures/headings-custom/src/content/docs/headings-stale-cache-check.mdoc
new file mode 100644
index 000000000..75cd52884
--- /dev/null
+++ b/packages/integrations/markdoc/test/fixtures/headings-custom/src/content/docs/headings-stale-cache-check.mdoc
@@ -0,0 +1,13 @@
+Our heading ID generator can have a stale cache for duplicates. Let's check for those!
+
+# Level 1 heading
+
+## Level **2 heading**
+
+### Level _3 heading_
+
+#### Level [4 heading](/with-a-link)
+
+##### Level 5 heading with override {% #id-override %}
+
+###### Level 6 heading
diff --git a/packages/integrations/markdoc/test/fixtures/headings-custom/src/pages/index.astro b/packages/integrations/markdoc/test/fixtures/headings-custom/src/pages/[slug].astro
index 5880be0e3..2baef9d69 100644
--- a/packages/integrations/markdoc/test/fixtures/headings-custom/src/pages/index.astro
+++ b/packages/integrations/markdoc/test/fixtures/headings-custom/src/pages/[slug].astro
@@ -1,8 +1,14 @@
---
-import { getEntryBySlug } from "astro:content";
+import { getCollection, CollectionEntry } from "astro:content";
-const post = await getEntryBySlug('docs', 'headings');
-const { Content, headings } = await post.render();
+export async function getStaticPaths() {
+ const docs = await getCollection('docs');
+ return docs.map(doc => ({ params: { slug: doc.slug }, props: doc }));
+}
+
+type Props = CollectionEntry<'docs'>;
+
+const { Content, headings } = await Astro.props.render();
---
<!DOCTYPE html>
diff --git a/packages/integrations/markdoc/test/fixtures/headings/src/content/docs/headings-stale-cache-check.mdoc b/packages/integrations/markdoc/test/fixtures/headings/src/content/docs/headings-stale-cache-check.mdoc
new file mode 100644
index 000000000..75cd52884
--- /dev/null
+++ b/packages/integrations/markdoc/test/fixtures/headings/src/content/docs/headings-stale-cache-check.mdoc
@@ -0,0 +1,13 @@
+Our heading ID generator can have a stale cache for duplicates. Let's check for those!
+
+# Level 1 heading
+
+## Level **2 heading**
+
+### Level _3 heading_
+
+#### Level [4 heading](/with-a-link)
+
+##### Level 5 heading with override {% #id-override %}
+
+###### Level 6 heading
diff --git a/packages/integrations/markdoc/test/fixtures/headings/src/pages/index.astro b/packages/integrations/markdoc/test/fixtures/headings/src/pages/[slug].astro
index 5880be0e3..2baef9d69 100644
--- a/packages/integrations/markdoc/test/fixtures/headings/src/pages/index.astro
+++ b/packages/integrations/markdoc/test/fixtures/headings/src/pages/[slug].astro
@@ -1,8 +1,14 @@
---
-import { getEntryBySlug } from "astro:content";
+import { getCollection, CollectionEntry } from "astro:content";
-const post = await getEntryBySlug('docs', 'headings');
-const { Content, headings } = await post.render();
+export async function getStaticPaths() {
+ const docs = await getCollection('docs');
+ return docs.map(doc => ({ params: { slug: doc.slug }, props: doc }));
+}
+
+type Props = CollectionEntry<'docs'>;
+
+const { Content, headings } = await Astro.props.render();
---
<!DOCTYPE html>
diff --git a/packages/integrations/markdoc/test/headings.test.js b/packages/integrations/markdoc/test/headings.test.js
index 5db50065c..5468e8c6b 100644
--- a/packages/integrations/markdoc/test/headings.test.js
+++ b/packages/integrations/markdoc/test/headings.test.js
@@ -27,7 +27,15 @@ describe('Markdoc - Headings', () => {
});
it('applies IDs to headings', async () => {
- const res = await fixture.fetch('/');
+ const res = await fixture.fetch('/headings');
+ const html = await res.text();
+ const { document } = parseHTML(html);
+
+ idTest(document);
+ });
+
+ it('generates the same IDs for other documents with the same headings', async () => {
+ const res = await fixture.fetch('/headings-stale-cache-check');
const html = await res.text();
const { document } = parseHTML(html);
@@ -35,7 +43,7 @@ describe('Markdoc - Headings', () => {
});
it('generates a TOC with correct info', async () => {
- const res = await fixture.fetch('/');
+ const res = await fixture.fetch('/headings');
const html = await res.text();
const { document } = parseHTML(html);
@@ -49,14 +57,21 @@ describe('Markdoc - Headings', () => {
});
it('applies IDs to headings', async () => {
- const html = await fixture.readFile('/index.html');
+ const html = await fixture.readFile('/headings/index.html');
+ const { document } = parseHTML(html);
+
+ idTest(document);
+ });
+
+ it('generates the same IDs for other documents with the same headings', async () => {
+ const html = await fixture.readFile('/headings-stale-cache-check/index.html');
const { document } = parseHTML(html);
idTest(document);
});
it('generates a TOC with correct info', async () => {
- const html = await fixture.readFile('/index.html');
+ const html = await fixture.readFile('/headings/index.html');
const { document } = parseHTML(html);
tocTest(document);
@@ -83,7 +98,15 @@ describe('Markdoc - Headings with custom Astro renderer', () => {
});
it('applies IDs to headings', async () => {
- const res = await fixture.fetch('/');
+ const res = await fixture.fetch('/headings');
+ const html = await res.text();
+ const { document } = parseHTML(html);
+
+ idTest(document);
+ });
+
+ it('generates the same IDs for other documents with the same headings', async () => {
+ const res = await fixture.fetch('/headings-stale-cache-check');
const html = await res.text();
const { document } = parseHTML(html);
@@ -91,7 +114,7 @@ describe('Markdoc - Headings with custom Astro renderer', () => {
});
it('generates a TOC with correct info', async () => {
- const res = await fixture.fetch('/');
+ const res = await fixture.fetch('/headings');
const html = await res.text();
const { document } = parseHTML(html);
@@ -99,7 +122,7 @@ describe('Markdoc - Headings with custom Astro renderer', () => {
});
it('renders Astro component for each heading', async () => {
- const res = await fixture.fetch('/');
+ const res = await fixture.fetch('/headings');
const html = await res.text();
const { document } = parseHTML(html);
@@ -113,21 +136,28 @@ describe('Markdoc - Headings with custom Astro renderer', () => {
});
it('applies IDs to headings', async () => {
- const html = await fixture.readFile('/index.html');
+ const html = await fixture.readFile('/headings/index.html');
+ const { document } = parseHTML(html);
+
+ idTest(document);
+ });
+
+ it('generates the same IDs for other documents with the same headings', async () => {
+ const html = await fixture.readFile('/headings-stale-cache-check/index.html');
const { document } = parseHTML(html);
idTest(document);
});
it('generates a TOC with correct info', async () => {
- const html = await fixture.readFile('/index.html');
+ const html = await fixture.readFile('/headings/index.html');
const { document } = parseHTML(html);
tocTest(document);
});
it('renders Astro component for each heading', async () => {
- const html = await fixture.readFile('/index.html');
+ const html = await fixture.readFile('/headings/index.html');
const { document } = parseHTML(html);
astroComponentTest(document);