Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions scripts/lib/ts-ext-register.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { existsSync } from 'node:fs'
import { register } from 'node:module'
import { dirname, join } from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'

export async function resolve(specifier, context, nextResolve) {
if (
(specifier.startsWith('./') || specifier.startsWith('../')) &&
!/\.[a-zA-Z][a-zA-Z0-9]*$/.test(specifier)
) {
const parentPath = context.parentURL ? fileURLToPath(context.parentURL) : process.cwd()
const candidate = join(dirname(parentPath), `${specifier}.ts`)
if (existsSync(candidate)) {
return {
shortCircuit: true,
url: pathToFileURL(candidate).href,
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
}
}

return nextResolve(specifier, context)
}

register(import.meta.url)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
147 changes: 147 additions & 0 deletions src/lib/agent-markdown/agent-markdown.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { test } from 'node:test'
import assert from 'node:assert/strict'
import { readFileSync } from 'node:fs'
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { cleanMarkdownFragment, normalizeLines } from './markdown.ts'
import { getParsedDoc, getParsedDocs, parseMdxSource, readFrontmatterValue } from './page-parser.ts'
import {
createSourceProvider,
getSourceProvider,
resolveImportSource,
resolveTemplateExport,
setSourceProvider,
} from './source-loader.ts'

const fixtureDir = dirname(fileURLToPath(import.meta.url))

function readFixture(name) {
return readFileSync(join(fixtureDir, 'fixtures', name), 'utf8')
}

function readGolden(name) {
return readFileSync(join(fixtureDir, 'fixtures', 'golden', name), 'utf8')
}

const simpleSource = readFixture('simple.mdx')
const componentsSource = readFixture('components.mdx')
const connectorSource = readFixture('connector.mdx')

const fixtureDocs = {
'/src/content/docs/fixtures/simple.mdx': simpleSource,
'/src/content/docs/fixtures/components.mdx': componentsSource,
'/src/content/docs/agentkit/connectors/github.mdx': connectorSource,
}

const fixtureTemplates = {
'/src/components/templates/agent-connectors/_setup-github.mdx': 'Set up GitHub.\n',
}

const fixtureTemplateIndex = {
'/src/components/templates/index.ts':
"export { default as SetupGithubSection } from './agent-connectors/_setup-github.mdx'\n",
}

setSourceProvider(
createSourceProvider({
docs: fixtureDocs,
templates: fixtureTemplates,
templateIndexModules: fixtureTemplateIndex,
}),
)

test('normalizeLines converts CRLF to LF', () => {
assert.equal(normalizeLines('a\r\nb\r\n'), 'a\nb\n')
})

test('injected fixtures replace the full docs tree', () => {
const routes = getParsedDocs()
.map((doc) => doc.route)
.sort()
assert.deepEqual(routes, ['agentkit/connectors/github', 'fixtures/components', 'fixtures/simple'])
})

test('parseMdxSource locks simple fixture metadata', () => {
const parsed = parseMdxSource('/src/content/docs/fixtures/simple.mdx', simpleSource)
assert.equal(parsed.route, 'fixtures/simple')
assert.equal(parsed.title, 'Simple fixture')
assert.equal(parsed.description, 'A page with no custom components')
assert.deepEqual(parsed.imports, [])
assert.deepEqual(parsed.componentNames, [])
})

test('parseMdxSource locks component fixture imports', () => {
const parsed = parseMdxSource('/src/content/docs/fixtures/components.mdx', componentsSource)
assert.equal(parsed.route, 'fixtures/components')
assert.deepEqual(parsed.imports.map((binding) => binding.localName).sort(), [
'Aside',
'Steps',
'TabItem',
'Tabs',
])
assert.deepEqual(parsed.componentNames.sort(), ['Aside', 'Steps', 'TabItem', 'Tabs'])
})

test('parseMdxSource resolves @/ imports on the connector fixture', () => {
const parsed = getParsedDoc('agentkit/connectors/github')
assert.ok(parsed)
assert.equal(parsed.title, 'GitHub connector')
assert.equal(readFrontmatterValue(parsed.frontmatter, 'connectorAuthType'), 'OAuth 2.0')
assert.deepEqual(
parsed.imports.map((binding) => [binding.localName, binding.resolvedSource]),
[
['CheckItem', '/src/components/ui/CheckItem.astro'],
['SetupGithubSection', '/src/components/templates/index.ts'],
],
)
})

test('resolveImportSource and template export map use the injected provider', () => {
assert.equal(
resolveImportSource(
'@/components/ui/CheckItem.astro',
'/src/content/docs/fixtures/connector.mdx',
),
'/src/components/ui/CheckItem.astro',
)
assert.equal(
resolveTemplateExport('SetupGithubSection'),
'/src/components/templates/agent-connectors/_setup-github.mdx',
)
})

test('cleanMarkdownFragment locks simple fixture output', () => {
const parsed = parseMdxSource('/src/content/docs/fixtures/simple.mdx', simpleSource)
assert.equal(cleanMarkdownFragment(parsed.body) + '\n', readGolden('simple.md'))
})

test('cleanMarkdownFragment locks component fixture output', () => {
const parsed = parseMdxSource('/src/content/docs/fixtures/components.mdx', componentsSource)
assert.equal(cleanMarkdownFragment(parsed.body) + '\n', readGolden('components.md'))
})

test('cleanMarkdownFragment locks connector fixture output', () => {
const parsed = parseMdxSource('/src/content/docs/agentkit/connectors/github.mdx', connectorSource)
assert.equal(cleanMarkdownFragment(parsed.body) + '\n', readGolden('connector.md'))
})

test('setSourceProvider rebuilds parsed docs from a new map', () => {
const previous = getSourceProvider()

setSourceProvider(
createSourceProvider({
docs: {
'/src/content/docs/only.mdx': '---\ntitle: Only\ndescription: One page\n---\n\nHello.\n',
},
}),
)

try {
const docs = getParsedDocs()
assert.equal(docs.length, 1)
assert.equal(docs[0].route, 'only')
assert.equal(docs[0].title, 'Only')
} finally {
setSourceProvider(previous)
}
})
35 changes: 35 additions & 0 deletions src/lib/agent-markdown/fixtures/components.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
---
title: 'Component fixture'
description: 'A page that uses Starlight components'
---
Comment thread
coderabbitai[bot] marked this conversation as resolved.

import { Aside, Tabs, TabItem, Steps } from '@astrojs/starlight/components'

Intro text.

<Aside type="note" title="Important claims to validate">
Always verify `iss`, `aud`, and `exp` claims.
</Aside>

<Tabs>
<TabItem label="Node.js">

Use `scalekit`.

</TabItem>
<TabItem label="Python">

Use `scalekit_client`.

</TabItem>
</Tabs>

<Steps>

1. Install the SDK

2. Create a client

</Steps>
Comment thread
coderabbitai[bot] marked this conversation as resolved.

![Login screen](/images/login.png)
17 changes: 17 additions & 0 deletions src/lib/agent-markdown/fixtures/connector.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
title: 'GitHub connector'
description: 'Connect GitHub to your agent'
connectorAuthType: 'OAuth 2.0'
connectorCategories: [developer_tools, project_management]
---

import CheckItem from '@/components/ui/CheckItem.astro'
import { SetupGithubSection } from '@/components/templates'

Connect the GitHub account.

<CheckItem href="/agentkit/overview/">
Read the overview
</CheckItem>

<SetupGithubSection />
19 changes: 19 additions & 0 deletions src/lib/agent-markdown/fixtures/golden/components.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Intro text.

> note: Important claims to validate
>
> Always verify `iss`, `aud`, and `exp` claims.

### Node.js

Use `scalekit`.

### Python

Use `scalekit_client`.

1. Install the SDK

2. Create a client

> Image: Login screen
3 changes: 3 additions & 0 deletions src/lib/agent-markdown/fixtures/golden/connector.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Connect the GitHub account.

Read the overview
3 changes: 3 additions & 0 deletions src/lib/agent-markdown/fixtures/golden/simple.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Use the Scalekit SDK to create a session.

See the [status page](https://scalekit.statuspage.io/).
10 changes: 10 additions & 0 deletions src/lib/agent-markdown/fixtures/simple.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
title: 'Simple fixture'
description: 'A page with no custom components'
sidebar:
label: 'Simple'
---

Use the Scalekit SDK to create a session.

See the [status page](https://scalekit.statuspage.io/).
42 changes: 29 additions & 13 deletions src/lib/agent-markdown/page-parser.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { getDocSourceEntries, resolveImportSource } from './source-loader'
import { normalizeLines } from './markdown'
import {
getDocSourceEntries,
getSourceProvider,
resolveImportSource,
type SourceProvider,
} from './source-loader'
import type { ComponentUsage, ImportBinding, ParsedMdxFile } from './types'

function normalizeLines(value: string): string {
return value.replace(/\r\n/g, '\n')
}

export function splitFrontmatter(raw: string): { frontmatter: string; body: string } {
const match = normalizeLines(raw).match(/^---\n([\s\S]*?)\n---\n?([\s\S]*)$/)
if (!match) {
Expand Down Expand Up @@ -241,17 +243,31 @@ export function parseMdxSource(filePath: string, source: string): ParsedMdxFile
}
}

const parsedDocs = new Map<string, ParsedMdxFile>(
getDocSourceEntries().map(([filePath, source]) => {
const parsed = parseMdxSource(filePath, source)
return [parsed.route, parsed]
}),
)
let parsedDocsCache: {
provider: SourceProvider
docs: Map<string, ParsedMdxFile>
} | null = null

function getParsedDocsMap(): Map<string, ParsedMdxFile> {
const provider = getSourceProvider()
if (parsedDocsCache?.provider === provider) {
return parsedDocsCache.docs
}

const docs = new Map<string, ParsedMdxFile>(
getDocSourceEntries().map(([filePath, source]) => {
const parsed = parseMdxSource(filePath, source)
return [parsed.route, parsed]
}),
)
parsedDocsCache = { provider, docs }
return docs
}

export function getParsedDoc(route: string): ParsedMdxFile | undefined {
return parsedDocs.get(route)
return getParsedDocsMap().get(route)
}

export function getParsedDocs(): ParsedMdxFile[] {
return Array.from(parsedDocs.values())
return Array.from(getParsedDocsMap().values())
}
Loading