diff --git a/packages/markdown/remark/package.json b/packages/markdown/remark/package.json index 55b5e17b78ea..b020022483db 100644 --- a/packages/markdown/remark/package.json +++ b/packages/markdown/remark/package.json @@ -34,7 +34,8 @@ "build": "astro-scripts build \"src/**/*.ts\" && tsc -p tsconfig.json", "build:ci": "astro-scripts build \"src/**/*.ts\"", "dev": "astro-scripts dev \"src/**/*.ts\"", - "test": "astro-scripts test \"test/**/*.test.js\"" + "test": "astro-scripts test \"test/**/*.test.ts\"", + "typecheck:tests": "tsc -p tsconfig.test.json --noEmit" }, "dependencies": { "@astrojs/internal-helpers": "workspace:*", diff --git a/packages/markdown/remark/test/autolinking.test.js b/packages/markdown/remark/test/autolinking.test.ts similarity index 91% rename from packages/markdown/remark/test/autolinking.test.js rename to packages/markdown/remark/test/autolinking.test.ts index 3fd5ad0fcdad..2eb5acf5570e 100644 --- a/packages/markdown/remark/test/autolinking.test.js +++ b/packages/markdown/remark/test/autolinking.test.ts @@ -1,10 +1,10 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; -import { createMarkdownProcessor } from '../dist/index.js'; +import { createMarkdownProcessor, type MarkdownProcessor } from '../dist/index.js'; describe('autolinking', () => { describe('plain md', () => { - let processor; + let processor: MarkdownProcessor; before(async () => { processor = await createMarkdownProcessor(); diff --git a/packages/markdown/remark/test/browser.test.js b/packages/markdown/remark/test/browser.test.ts similarity index 92% rename from packages/markdown/remark/test/browser.test.js rename to packages/markdown/remark/test/browser.test.ts index 824f6fa0becd..9a7f5b0c3cd8 100644 --- a/packages/markdown/remark/test/browser.test.js +++ b/packages/markdown/remark/test/browser.test.ts @@ -14,7 +14,7 @@ describe('Bundle for browsers', async () => { assert.ok(result.outputFiles.length > 0); } catch (error) { // Capture any esbuild errors and fail the test - assert.fail(error.message); + assert.fail((error as Error).message); } }); }); diff --git a/packages/markdown/remark/test/entities.test.js b/packages/markdown/remark/test/entities.test.ts similarity index 80% rename from packages/markdown/remark/test/entities.test.js rename to packages/markdown/remark/test/entities.test.ts index 3c244c15abb4..1ae4aea90df8 100644 --- a/packages/markdown/remark/test/entities.test.js +++ b/packages/markdown/remark/test/entities.test.ts @@ -1,9 +1,9 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; -import { createMarkdownProcessor } from '../dist/index.js'; +import { createMarkdownProcessor, type MarkdownProcessor } from '../dist/index.js'; describe('entities', async () => { - let processor; + let processor: MarkdownProcessor; before(async () => { processor = await createMarkdownProcessor(); diff --git a/packages/markdown/remark/test/frontmatter.test.js b/packages/markdown/remark/test/frontmatter.test.ts similarity index 93% rename from packages/markdown/remark/test/frontmatter.test.js rename to packages/markdown/remark/test/frontmatter.test.ts index 336245106d1e..ee365d19e4de 100644 --- a/packages/markdown/remark/test/frontmatter.test.js +++ b/packages/markdown/remark/test/frontmatter.test.ts @@ -1,6 +1,12 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; -import { extractFrontmatter, parseFrontmatter } from '../dist/index.js'; +import { + extractFrontmatter, + parseFrontmatter, + type ParseFrontmatterOptions, +} from '../dist/index.js'; + +type FrontmatterStyle = ParseFrontmatterOptions['frontmatter']; const bom = '\uFEFF'; @@ -157,13 +163,14 @@ describe('parseFrontmatter', () => { it('frontmatter style for YAML', () => { const yaml = `\nfoo: bar\n`; - const parse1 = (style) => parseFrontmatter(`---${yaml}---`, { frontmatter: style }).content; + const parse1 = (style: FrontmatterStyle) => + parseFrontmatter(`---${yaml}---`, { frontmatter: style }).content; assert.deepEqual(parse1('preserve'), `---${yaml}---`); assert.deepEqual(parse1('remove'), ''); assert.deepEqual(parse1('empty-with-spaces'), ` \n \n `); assert.deepEqual(parse1('empty-with-lines'), `\n\n`); - const parse2 = (style) => + const parse2 = (style: FrontmatterStyle) => parseFrontmatter(`\n \n---${yaml}---\n\ncontent`, { frontmatter: style }).content; assert.deepEqual(parse2('preserve'), `\n \n---${yaml}---\n\ncontent`); assert.deepEqual(parse2('remove'), '\n \n\n\ncontent'); @@ -173,13 +180,14 @@ describe('parseFrontmatter', () => { it('frontmatter style for TOML', () => { const toml = `\nfoo = "bar"\n`; - const parse1 = (style) => parseFrontmatter(`+++${toml}+++`, { frontmatter: style }).content; + const parse1 = (style: FrontmatterStyle) => + parseFrontmatter(`+++${toml}+++`, { frontmatter: style }).content; assert.deepEqual(parse1('preserve'), `+++${toml}+++`); assert.deepEqual(parse1('remove'), ''); assert.deepEqual(parse1('empty-with-spaces'), ` \n \n `); assert.deepEqual(parse1('empty-with-lines'), `\n\n`); - const parse2 = (style) => + const parse2 = (style: FrontmatterStyle) => parseFrontmatter(`\n \n+++${toml}+++\n\ncontent`, { frontmatter: style }).content; assert.deepEqual(parse2('preserve'), `\n \n+++${toml}+++\n\ncontent`); assert.deepEqual(parse2('remove'), '\n \n\n\ncontent'); diff --git a/packages/markdown/remark/test/highlight.test.js b/packages/markdown/remark/test/highlight.test.ts similarity index 100% rename from packages/markdown/remark/test/highlight.test.js rename to packages/markdown/remark/test/highlight.test.ts diff --git a/packages/markdown/remark/test/plugins.test.js b/packages/markdown/remark/test/plugins.test.ts similarity index 62% rename from packages/markdown/remark/test/plugins.test.js rename to packages/markdown/remark/test/plugins.test.ts index c52955f83902..9d0249faf813 100644 --- a/packages/markdown/remark/test/plugins.test.js +++ b/packages/markdown/remark/test/plugins.test.ts @@ -1,28 +1,26 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; import { fileURLToPath } from 'node:url'; -import { createMarkdownProcessor } from '../dist/index.js'; +import type { VFile } from 'vfile'; +import { createMarkdownProcessor, type RemarkPlugin } from '../dist/index.js'; describe('plugins', () => { it('should be able to get file path when passing fileURL', async () => { - let context; + let context: VFile | undefined; + + const collectFile: RemarkPlugin = () => (_tree, file) => { + context = file; + }; const processor = await createMarkdownProcessor({ - remarkPlugins: [ - () => { - const transformer = (_tree, file) => { - context = file; - }; - return transformer; - }, - ], + remarkPlugins: [collectFile], }); await processor.render(`test`, { fileURL: new URL('virtual.md', import.meta.url), }); - assert.ok(typeof context === 'object'); + assert.ok(context); assert.equal(context.path, fileURLToPath(new URL('virtual.md', import.meta.url))); }); }); diff --git a/packages/markdown/remark/test/prism.test.js b/packages/markdown/remark/test/prism.test.ts similarity index 100% rename from packages/markdown/remark/test/prism.test.js rename to packages/markdown/remark/test/prism.test.ts diff --git a/packages/markdown/remark/test/remark-collect-images.test.js b/packages/markdown/remark/test/remark-collect-images.test.ts similarity index 81% rename from packages/markdown/remark/test/remark-collect-images.test.js rename to packages/markdown/remark/test/remark-collect-images.test.ts index c8652642cde5..579a7b7848ef 100644 --- a/packages/markdown/remark/test/remark-collect-images.test.js +++ b/packages/markdown/remark/test/remark-collect-images.test.ts @@ -1,33 +1,36 @@ import assert from 'node:assert/strict'; import { before, describe, it } from 'node:test'; import { visit } from 'unist-util-visit'; -import { createMarkdownProcessor } from '../dist/index.js'; +import { + createMarkdownProcessor, + type MarkdownProcessor, + type RehypePlugin, +} from '../dist/index.js'; describe('collect images', async () => { - let processor; - let processorWithHastProperties; + let processor: MarkdownProcessor; + let processorWithHastProperties: MarkdownProcessor; before(async () => { processor = await createMarkdownProcessor({ image: { domains: ['example.com'] } }); + + const addImageProps: RehypePlugin = () => (tree) => { + visit(tree, 'element', (node) => { + if (node.tagName === 'img') { + node.properties.className = ['image-class']; + node.properties.htmlFor = 'some-id'; + } + }); + }; + processorWithHastProperties = await createMarkdownProcessor({ - rehypePlugins: [ - () => { - return (tree) => { - visit(tree, 'element', (node) => { - if (node.tagName === 'img') { - node.properties.className = ['image-class']; - node.properties.htmlFor = 'some-id'; - } - }); - }; - }, - ], + rehypePlugins: [addImageProps], }); }); it('should collect inline image paths', async () => { const markdown = `Hello ![inline image url](./img.png)`; - const fileURL = 'file.md'; + const fileURL = new URL('file.md', import.meta.url); const { code, @@ -45,7 +48,7 @@ describe('collect images', async () => { it('should collect allowed remote image paths', async () => { const markdown = `Hello ![inline remote image url](https://example.com/example.png)`; - const fileURL = 'file.md'; + const fileURL = new URL('file.md', import.meta.url); const { code, @@ -62,7 +65,7 @@ describe('collect images', async () => { it('should not collect other remote image paths', async () => { const markdown = `Hello ![inline remote image url](https://google.com/google.png)`; - const fileURL = 'file.md'; + const fileURL = new URL('file.md', import.meta.url); const { code, @@ -79,7 +82,7 @@ describe('collect images', async () => { it('should add image paths from definition', async () => { const markdown = `Hello ![image ref][img-ref] ![remote image ref][remote-img-ref]\n\n[img-ref]: ./img.webp\n[remote-img-ref]: https://example.com/example.jpg`; - const fileURL = 'file.md'; + const fileURL = new URL('file.md', import.meta.url); const { code, metadata } = await processor.render(markdown, { fileURL }); @@ -94,7 +97,7 @@ describe('collect images', async () => { it('should preserve className as HTML class attribute', async () => { const markdown = `Hello ![image with class](./img.png)`; - const fileURL = 'file.md'; + const fileURL = new URL('file.md', import.meta.url); const { code } = await processorWithHastProperties.render(markdown, { fileURL }); diff --git a/packages/markdown/remark/test/shiki.test.js b/packages/markdown/remark/test/shiki.test.ts similarity index 78% rename from packages/markdown/remark/test/shiki.test.js rename to packages/markdown/remark/test/shiki.test.ts index 56ffe77a5015..bc6d0dcb2fe3 100644 --- a/packages/markdown/remark/test/shiki.test.js +++ b/packages/markdown/remark/test/shiki.test.ts @@ -1,6 +1,13 @@ import assert from 'node:assert/strict'; import { describe, it } from 'node:test'; -import { createMarkdownProcessor, createShikiHighlighter } from '../dist/index.js'; +import type { Element } from 'hast'; +import type { LanguageRegistration, ThemeRegistration } from 'shiki'; +import { + createMarkdownProcessor, + createShikiHighlighter, + type ShikiHighlighter, +} from '../dist/index.js'; +// @ts-expect-error: `clearShikiHighlighterCache` is marked `@internal` and stripped from the `.d.ts`, but still exists at runtime. import { clearShikiHighlighterCache } from '../dist/shiki.js'; describe('shiki syntax highlighting', () => { @@ -44,9 +51,10 @@ describe('shiki syntax highlighting', () => { const highlighter = await createShikiHighlighter(); const hast = await highlighter.codeToHast('const foo = "bar";', 'js'); + const root = hast.children[0] as Element; - assert.match(hast.children[0].properties.class, /astro-code github-dark/); - assert.match(hast.children[0].properties.style, /background-color:#24292e;color:#e1e4e8;/); + assert.match(root.properties.class as string, /astro-code github-dark/); + assert.match(root.properties.style as string, /background-color:#24292e;color:#e1e4e8;/); }); it('createShikiHighlighter can reuse the same instance for different languages', async () => { @@ -76,11 +84,16 @@ describe('shiki syntax highlighting', () => { 'bicep', 'blade', 'bsl', - ]; + ] as const; - const highlighters = new Set(); + const highlighters = new Set(); for (const lang of langs) { - highlighters.add(await createShikiHighlighter({ langs: [lang] })); + highlighters.add( + await createShikiHighlighter({ + // @ts-expect-error: `langs` is typed as `LanguageRegistration[]`, but Shiki's `createHighlighter` accepts both strings and objects in the array. + langs: [lang], + }), + ); } // Ensure that we only have one highlighter instance. @@ -113,7 +126,11 @@ describe('shiki syntax highlighting', () => { const highlighter = await createShikiHighlighter(); const html = await highlighter.codeToHtml(`foo`, 'js', { - attributes: { 'data-foo': 'bar', autofocus: true }, + attributes: { + 'data-foo': 'bar', + // @ts-expect-error: Shiki's `codeToHtml` accepts boolean attributes as `string | boolean`, but the types are currently incorrect. + autofocus: true, + }, }); assert.match(html, /data-foo="bar"/); @@ -164,7 +181,7 @@ describe('shiki syntax highlighting', () => { }); const html = await highlighter.codeToHtml(`let test = "some string"`, 'cjs', { - attributes: { 'data-foo': 'bar', autofocus: true }, + attributes: { 'data-foo': 'bar' }, }); assert.match(html, /data-language="cjs"/); @@ -188,12 +205,15 @@ describe('shiki syntax highlighting', () => { clearShikiHighlighterCache(); const theme = 'github-light'; - const highlighter = await createShikiHighlighter({ theme }); + interface ShikiHighlighterInternal extends ShikiHighlighter { + loadLanguage(...langs: unknown[]): Promise; + getLoadedLanguages(): string[]; + } + const highlighter = (await createShikiHighlighter({ theme })) as ShikiHighlighterInternal; - // loadLanguage is an internal method - const loadLanguageArgs = []; - const originalLoadLanguage = highlighter['loadLanguage']; - highlighter['loadLanguage'] = async (...args) => { + const loadLanguageArgs: unknown[] = []; + const originalLoadLanguage = highlighter.loadLanguage; + highlighter.loadLanguage = async (...args: unknown[]) => { loadLanguageArgs.push(...args); return await originalLoadLanguage(...args); }; @@ -202,19 +222,35 @@ describe('shiki syntax highlighting', () => { assert.equal(loadLanguageArgs.length, 0); // Load a new language - const h1 = await createShikiHighlighter({ theme, langs: ['js'] }); + const h1 = await createShikiHighlighter({ + theme, + // @ts-expect-error: `langs` is typed as `LanguageRegistration[]`, but Shiki's `createHighlighter` accepts both strings and objects in the array. + langs: ['js'], + }); assert.equal(loadLanguageArgs.length, 1); // Load the same language again - const h2 = await createShikiHighlighter({ theme, langs: ['js'] }); + const h2 = await createShikiHighlighter({ + theme, + // @ts-expect-error: `langs` is typed as `LanguageRegistration[]`, but Shiki's `createHighlighter` accepts both strings and objects in the array. + langs: ['js'], + }); assert.equal(loadLanguageArgs.length, 1); // Load another language - const h3 = await createShikiHighlighter({ theme, langs: ['ts'] }); + const h3 = await createShikiHighlighter({ + theme, + // @ts-expect-error: `langs` is typed as `LanguageRegistration[]`, but Shiki's `createHighlighter` accepts both strings and objects in the array. + langs: ['ts'], + }); assert.equal(loadLanguageArgs.length, 2); // Load the same language again - const h4 = await createShikiHighlighter({ theme, langs: ['ts'] }); + const h4 = await createShikiHighlighter({ + theme, + // @ts-expect-error: `langs` is typed as `LanguageRegistration[]`, but Shiki's `createHighlighter` accepts both strings and objects in the array. + langs: ['ts'], + }); assert.equal(loadLanguageArgs.length, 2); // All highlighters should be the same instance @@ -251,7 +287,7 @@ describe('shiki syntax highlighting', () => { it('uses a custom (ThemeRegistrationRaw) theme', async () => { // Minimal subset of a custom theme — only the fields Shiki needs to // derive the pre element's background-color and color. - const serendipityMorning = { + const serendipityMorning: ThemeRegistration = { name: 'Serendipity Morning', type: 'light', colors: { @@ -279,7 +315,7 @@ describe('shiki syntax highlighting', () => { // Minimal rinfo grammar — same language used in the langs fixture. // Must be passed as a LanguageRegistration (name + scopeName at top level), // not the { id, grammar } wrapper used by Astro's config layer. - const riLang = { + const riLang: LanguageRegistration = { name: 'rinfo', scopeName: 'source.rinfo', patterns: [{ include: '#lf-rinfo' }], diff --git a/packages/markdown/remark/tsconfig.test.json b/packages/markdown/remark/tsconfig.test.json new file mode 100644 index 000000000000..fa4f11e4ae8b --- /dev/null +++ b/packages/markdown/remark/tsconfig.test.json @@ -0,0 +1,12 @@ +{ + "extends": "../../../tsconfig.base.json", + "include": ["test/**/*.ts"], + "exclude": ["test/fixtures/**"], + "compilerOptions": { + "allowJs": true, + "noUnusedLocals": false, + "noUnusedParameters": false, + "rewriteRelativeImportExtensions": true + }, + "references": [{ "path": "../../astro/tsconfig.test.json" }] +}