pulumi-package-docs 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +98 -4
- package/bin/args.js +54 -3
- package/bin/args.test.js +50 -0
- package/bin/cli-bun.js +54 -22
- package/bin/cli.js +74 -28
- package/bin/cli.test.js +18 -0
- package/bin/static.js +196 -0
- package/bin/static.test.js +176 -0
- package/dist/client/assets/MemberDetail-D8jkDkVP.js +1 -0
- package/dist/client/assets/{token-oQmZfAP3.js → examples-zkHFEaMt.js} +1 -1
- package/dist/client/assets/index-DM9VsIdh.js +13 -0
- package/dist/client/assets/providers._name-C-FSbmVZ.js +1 -0
- package/dist/client/assets/providers._name.functions._token-B3S8Gcfx.js +1 -0
- package/dist/client/assets/providers._name.functions._token-C4VPHGlG.js +1 -0
- package/dist/client/assets/providers._name.index-CWHPtoBD.js +1 -0
- package/dist/client/assets/providers._name.index-nlnQuJ9Q.js +1 -0
- package/dist/client/assets/providers._name.resources._token-BISpZVHI.js +1 -0
- package/dist/client/assets/providers._name.resources._token-CvMi94a3.js +1 -0
- package/dist/client/assets/{routes-DKNWlgPZ.js → routes-DwhZQHE3.js} +1 -1
- package/dist/mcp/stdio.js +449 -0
- package/dist/server/assets/{MemberDetail-C5sAHNZW.js → MemberDetail-CdHUSmQD.js} +2 -1
- package/dist/server/assets/{_tanstack-start-manifest_v-B0J-S-em.js → _tanstack-start-manifest_v-DNIp6gp3.js} +13 -15
- package/dist/server/assets/api-D49ZDhMh.js +27 -0
- package/dist/server/assets/{providers._name.functions._token-Gkw4zt3M.js → providers._name.functions._token-BvkwmQ-L.js} +3 -2
- package/dist/server/assets/{providers._name.index-CEXkl-76.js → providers._name.index-DBkfEjsy.js} +3 -2
- package/dist/server/assets/{providers._name.resources._token-R_i2MmRl.js → providers._name.resources._token-Sl31Qlvd.js} +3 -2
- package/dist/server/assets/{schema.server-B-VvU1qb.js → providers.server-B1ZHZ5jE.js} +48 -1
- package/dist/server/assets/{router-1INob9fh.js → router-_zs8P-B2.js} +24 -87
- package/dist/server/assets/{routes-4jiXnS1p.js → routes-BUxagg9r.js} +1 -1
- package/dist/server/assets/token-DBEFeZIN.js +92 -0
- package/dist/server/server.js +4 -4
- package/examples/github-pages.yml +56 -0
- package/examples/gitlab-ci.yml +22 -0
- package/package.json +6 -2
- package/src/components/ThemeToggle.tsx +37 -0
- package/src/lib/pulumi/api.ts +18 -69
- package/src/lib/pulumi/mcp.server.test.ts +112 -0
- package/src/lib/pulumi/mcp.server.ts +11 -6
- package/src/lib/pulumi/providers.server.ts +80 -0
- package/src/lib/pulumi/stdio.server.ts +16 -0
- package/src/lib/pulumi/token.test.ts +44 -1
- package/src/lib/pulumi/token.ts +28 -0
- package/src/lib/static-docs.ts +15 -0
- package/src/router.tsx +3 -0
- package/src/routes/__root.tsx +6 -3
- package/src/routes/providers.$name.functions.$token.tsx +7 -4
- package/src/routes/providers.$name.index.tsx +2 -2
- package/src/routes/providers.$name.resources.$token.tsx +7 -4
- package/vite.config.ts +41 -1
- package/dist/client/assets/MemberDetail-BS9DbqEt.js +0 -1
- package/dist/client/assets/index-2FzxAhzN.js +0 -13
- package/dist/client/assets/providers._name-FmJEVkdV.js +0 -1
- package/dist/client/assets/providers._name.functions._token-BzjmNHea.js +0 -1
- package/dist/client/assets/providers._name.functions._token-D768U6I-.js +0 -1
- package/dist/client/assets/providers._name.index-C42yQ95z.js +0 -1
- package/dist/client/assets/providers._name.index-Dm0YiZWP.js +0 -1
- package/dist/client/assets/providers._name.resources._token-CIYPoqyA.js +0 -1
- package/dist/client/assets/providers._name.resources._token-D0OLvgo_.js +0 -1
- package/dist/server/assets/api-C55bS4R6.js +0 -64
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { join } from 'node:path'
|
|
4
|
+
|
|
5
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js'
|
|
6
|
+
import { InMemoryTransport } from '@modelcontextprotocol/sdk/inMemory.js'
|
|
7
|
+
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
8
|
+
|
|
9
|
+
import { createMcpServer } from '@/lib/pulumi/mcp.server'
|
|
10
|
+
|
|
11
|
+
let projectDir: string
|
|
12
|
+
const originalEnv = process.env.PULUMI_LOCAL_DOCS_DIR
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
projectDir = mkdtempSync(join(tmpdir(), 'pulumi-package-docs-mcp-test-'))
|
|
16
|
+
writeFileSync(
|
|
17
|
+
join(projectDir, 'Pulumi.yaml'),
|
|
18
|
+
`name: my-project\nruntime: nodejs\n`,
|
|
19
|
+
)
|
|
20
|
+
process.env.PULUMI_LOCAL_DOCS_DIR = projectDir
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
rmSync(projectDir, { recursive: true, force: true })
|
|
25
|
+
if (originalEnv === undefined) delete process.env.PULUMI_LOCAL_DOCS_DIR
|
|
26
|
+
else process.env.PULUMI_LOCAL_DOCS_DIR = originalEnv
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Drives the MCP server the same way the stdio entry point does — a plain in-process
|
|
31
|
+
* server with a transport attached, no HTTP server and no TanStack Start runtime.
|
|
32
|
+
*/
|
|
33
|
+
async function connectClient(): Promise<Client> {
|
|
34
|
+
const [clientTransport, serverTransport] =
|
|
35
|
+
InMemoryTransport.createLinkedPair()
|
|
36
|
+
const client = new Client({ name: 'test-client', version: '0.0.0' })
|
|
37
|
+
await Promise.all([
|
|
38
|
+
createMcpServer().connect(serverTransport),
|
|
39
|
+
client.connect(clientTransport),
|
|
40
|
+
])
|
|
41
|
+
return client
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function parseTextResult(result: unknown): unknown {
|
|
45
|
+
const { content } = result as {
|
|
46
|
+
content?: Array<{ type: string; text: string }>
|
|
47
|
+
}
|
|
48
|
+
expect(content?.[0]?.type).toBe('text')
|
|
49
|
+
return JSON.parse(content![0]!.text)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe('createMcpServer', () => {
|
|
53
|
+
it('advertises the provider docs tools', async () => {
|
|
54
|
+
const client = await connectClient()
|
|
55
|
+
|
|
56
|
+
const { tools } = await client.listTools()
|
|
57
|
+
|
|
58
|
+
expect(tools.map((tool) => tool.name).sort()).toEqual([
|
|
59
|
+
'get_function',
|
|
60
|
+
'get_resource',
|
|
61
|
+
'list_functions',
|
|
62
|
+
'list_providers',
|
|
63
|
+
'list_resources',
|
|
64
|
+
'search_members',
|
|
65
|
+
])
|
|
66
|
+
await client.close()
|
|
67
|
+
})
|
|
68
|
+
|
|
69
|
+
it('reports the discovered project from list_providers', async () => {
|
|
70
|
+
const client = await connectClient()
|
|
71
|
+
|
|
72
|
+
const result = await client.callTool({ name: 'list_providers' })
|
|
73
|
+
|
|
74
|
+
expect(parseTextResult(result)).toMatchObject({
|
|
75
|
+
targetDir: projectDir,
|
|
76
|
+
project: { root: projectDir, name: 'my-project', runtime: 'nodejs' },
|
|
77
|
+
providers: [],
|
|
78
|
+
})
|
|
79
|
+
await client.close()
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('returns no matches from search_members when the project has no packages', async () => {
|
|
83
|
+
const client = await connectClient()
|
|
84
|
+
|
|
85
|
+
const result = await client.callTool({
|
|
86
|
+
name: 'search_members',
|
|
87
|
+
arguments: { query: 'bucket' },
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
expect(parseTextResult(result)).toEqual({
|
|
91
|
+
query: 'bucket',
|
|
92
|
+
matchCount: 0,
|
|
93
|
+
matches: [],
|
|
94
|
+
})
|
|
95
|
+
await client.close()
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
it('surfaces a tool error for a provider that is not declared in Pulumi.yaml', async () => {
|
|
99
|
+
const client = await connectClient()
|
|
100
|
+
|
|
101
|
+
const result = await client.callTool({
|
|
102
|
+
name: 'list_resources',
|
|
103
|
+
arguments: { provider: 'nope' },
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
expect(result.isError).toBe(true)
|
|
107
|
+
expect(JSON.stringify(result.content)).toContain(
|
|
108
|
+
'No local provider named \\"nope\\"',
|
|
109
|
+
)
|
|
110
|
+
await client.close()
|
|
111
|
+
})
|
|
112
|
+
})
|
|
@@ -2,8 +2,6 @@ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
|
|
|
2
2
|
import type { CallToolResult } from '@modelcontextprotocol/sdk/types.js'
|
|
3
3
|
import { z } from 'zod'
|
|
4
4
|
|
|
5
|
-
import { getProvider, listProviders } from '@/lib/pulumi/api'
|
|
6
|
-
import type { ProviderDetail } from '@/lib/pulumi/api'
|
|
7
5
|
import { resolveDescription } from '@/lib/pulumi/description'
|
|
8
6
|
import { getTargetDir, loadPulumiProject } from '@/lib/pulumi/discovery.server'
|
|
9
7
|
import {
|
|
@@ -14,6 +12,11 @@ import {
|
|
|
14
12
|
} from '@/lib/pulumi/examples'
|
|
15
13
|
import { formatType } from '@/lib/pulumi/format'
|
|
16
14
|
import { formatPropertyName } from '@/lib/pulumi/naming'
|
|
15
|
+
import {
|
|
16
|
+
loadProjectSummary,
|
|
17
|
+
loadProviderDetail,
|
|
18
|
+
} from '@/lib/pulumi/providers.server'
|
|
19
|
+
import type { ProviderDetail } from '@/lib/pulumi/providers.server'
|
|
17
20
|
import { resolveProviderSchema } from '@/lib/pulumi/schema.server'
|
|
18
21
|
import { tokenDisplayName } from '@/lib/pulumi/token'
|
|
19
22
|
import type {
|
|
@@ -30,7 +33,7 @@ function textResult(data: unknown): CallToolResult {
|
|
|
30
33
|
async function requireProvider(
|
|
31
34
|
providerName: string,
|
|
32
35
|
): Promise<ProviderDetail & { schema: NonNullable<ProviderDetail['schema']> }> {
|
|
33
|
-
const detail = await
|
|
36
|
+
const detail = await loadProviderDetail(providerName)
|
|
34
37
|
if (!detail) {
|
|
35
38
|
throw new Error(
|
|
36
39
|
`No local provider named "${providerName}" was found. Call list_providers to see the providers declared in this project's Pulumi.yaml.`,
|
|
@@ -100,8 +103,10 @@ function memberDetail(
|
|
|
100
103
|
}
|
|
101
104
|
|
|
102
105
|
/**
|
|
103
|
-
* Builds a fresh MCP server with the local-provider-docs tools registered.
|
|
104
|
-
*
|
|
106
|
+
* Builds a fresh MCP server with the local-provider-docs tools registered. Transport
|
|
107
|
+
* agnostic: `src/routes/mcp.ts` calls it once per HTTP request (so concurrent requests
|
|
108
|
+
* never share state), and `src/lib/pulumi/stdio.server.ts` calls it once for the
|
|
109
|
+
* lifetime of a stdio process.
|
|
105
110
|
*/
|
|
106
111
|
export function createMcpServer(): McpServer {
|
|
107
112
|
const server = new McpServer({
|
|
@@ -116,7 +121,7 @@ export function createMcpServer(): McpServer {
|
|
|
116
121
|
description:
|
|
117
122
|
"List the local/parameterized Pulumi providers declared in this project's Pulumi.yaml (added with `pulumi package add`), with their source, version, and resource/function counts.",
|
|
118
123
|
},
|
|
119
|
-
async () => textResult(await
|
|
124
|
+
async () => textResult(await loadProjectSummary()),
|
|
120
125
|
)
|
|
121
126
|
|
|
122
127
|
server.registerTool(
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { getTargetDir, loadPulumiProject } from '@/lib/pulumi/discovery.server'
|
|
2
|
+
import { resolveProviderSchema } from '@/lib/pulumi/schema.server'
|
|
3
|
+
import type { ProviderEntry, PulumiProject } from '@/lib/pulumi/types'
|
|
4
|
+
|
|
5
|
+
export interface ProviderSummary {
|
|
6
|
+
name: string
|
|
7
|
+
source: string
|
|
8
|
+
version?: string
|
|
9
|
+
parameters?: string[]
|
|
10
|
+
resourceCount: number
|
|
11
|
+
functionCount: number
|
|
12
|
+
origin: ProviderEntry['origin']
|
|
13
|
+
error?: string
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ProjectSummary {
|
|
17
|
+
targetDir: string
|
|
18
|
+
project: Pick<PulumiProject, 'root' | 'name' | 'runtime'> | null
|
|
19
|
+
providers: ProviderSummary[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ProviderDetail extends ProviderEntry {
|
|
23
|
+
/** The Pulumi project's runtime (e.g. "nodejs", "python", "go"), used to render Inputs/Outputs names in that language's casing. */
|
|
24
|
+
runtime?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The project + provider summaries shown on the index route and by the MCP
|
|
29
|
+
* `list_providers` tool. Plain async functions (no TanStack Start server-fn
|
|
30
|
+
* wrapper) so they can run either inside the web server or in the stdio MCP
|
|
31
|
+
* process, which has no HTTP server at all.
|
|
32
|
+
*/
|
|
33
|
+
export async function loadProjectSummary(): Promise<ProjectSummary> {
|
|
34
|
+
const targetDir = getTargetDir()
|
|
35
|
+
const project = loadPulumiProject(targetDir)
|
|
36
|
+
|
|
37
|
+
if (!project) {
|
|
38
|
+
return { targetDir, project: null, providers: [] }
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const entries = await Promise.all(
|
|
42
|
+
project.packages.map((ref) => resolveProviderSchema(project.root, ref)),
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
const providers: ProviderSummary[] = entries.map((entry) => ({
|
|
46
|
+
name: entry.ref.name,
|
|
47
|
+
source: entry.ref.source,
|
|
48
|
+
version: entry.ref.version,
|
|
49
|
+
parameters: entry.ref.parameters,
|
|
50
|
+
resourceCount: Object.keys(entry.schema?.resources ?? {}).length,
|
|
51
|
+
functionCount: Object.keys(entry.schema?.functions ?? {}).length,
|
|
52
|
+
origin: entry.origin,
|
|
53
|
+
error: entry.error,
|
|
54
|
+
}))
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
targetDir,
|
|
58
|
+
project: {
|
|
59
|
+
root: project.root,
|
|
60
|
+
name: project.name,
|
|
61
|
+
runtime: project.runtime,
|
|
62
|
+
},
|
|
63
|
+
providers,
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The fully resolved schema for one local provider, or null if the project or provider isn't found. */
|
|
68
|
+
export async function loadProviderDetail(
|
|
69
|
+
name: string,
|
|
70
|
+
): Promise<ProviderDetail | null> {
|
|
71
|
+
const targetDir = getTargetDir()
|
|
72
|
+
const project = loadPulumiProject(targetDir)
|
|
73
|
+
if (!project) return null
|
|
74
|
+
|
|
75
|
+
const ref = project.packages.find((p) => p.name === name)
|
|
76
|
+
if (!ref) return null
|
|
77
|
+
|
|
78
|
+
const entry = await resolveProviderSchema(project.root, ref)
|
|
79
|
+
return { ...entry, runtime: project.runtime }
|
|
80
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
2
|
+
|
|
3
|
+
import { createMcpServer } from '@/lib/pulumi/mcp.server'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Serves the same provider-docs tools as the `/mcp` HTTP route over stdio, for MCP
|
|
7
|
+
* clients that spawn a server process instead of connecting to a URL.
|
|
8
|
+
*
|
|
9
|
+
* Unlike the HTTP route this is a single long-lived server (one process, one client),
|
|
10
|
+
* and stdout is the protocol channel — nothing on this path may write to it, so any
|
|
11
|
+
* diagnostics belong on stderr.
|
|
12
|
+
*/
|
|
13
|
+
export async function runStdioMcpServer(): Promise<void> {
|
|
14
|
+
const server = createMcpServer()
|
|
15
|
+
await server.connect(new StdioServerTransport())
|
|
16
|
+
}
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
findTokenBySlug,
|
|
5
|
+
tokenDisplayName,
|
|
6
|
+
tokenSlug,
|
|
7
|
+
} from '@/lib/pulumi/token'
|
|
4
8
|
|
|
5
9
|
describe('tokenDisplayName', () => {
|
|
6
10
|
it('returns the segment after the last colon for a resource token', () => {
|
|
@@ -19,3 +23,42 @@ describe('tokenDisplayName', () => {
|
|
|
19
23
|
expect(tokenDisplayName('plainName')).toBe('plainName')
|
|
20
24
|
})
|
|
21
25
|
})
|
|
26
|
+
|
|
27
|
+
describe('tokenSlug', () => {
|
|
28
|
+
it('replaces the token separators with dashes', () => {
|
|
29
|
+
expect(tokenSlug('random:index/randomString:RandomString')).toBe(
|
|
30
|
+
'random-index-randomString-RandomString',
|
|
31
|
+
)
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('leaves an already-safe token unchanged', () => {
|
|
35
|
+
expect(tokenSlug('plainName')).toBe('plainName')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('collapses a run of unsafe characters into a single dash', () => {
|
|
39
|
+
expect(tokenSlug('aws:s3//bucket:Bucket')).toBe('aws-s3-bucket-Bucket')
|
|
40
|
+
})
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
describe('findTokenBySlug', () => {
|
|
44
|
+
const tokens = [
|
|
45
|
+
'random:index/randomString:RandomString',
|
|
46
|
+
'random:index/randomPet:RandomPet',
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
it('finds the token a slug was made from', () => {
|
|
50
|
+
expect(findTokenBySlug(tokens, 'random-index-randomPet-RandomPet')).toBe(
|
|
51
|
+
'random:index/randomPet:RandomPet',
|
|
52
|
+
)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('also accepts the raw token', () => {
|
|
56
|
+
expect(
|
|
57
|
+
findTokenBySlug(tokens, 'random:index/randomString:RandomString'),
|
|
58
|
+
).toBe('random:index/randomString:RandomString')
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
it('returns undefined when nothing matches', () => {
|
|
62
|
+
expect(findTokenBySlug(tokens, 'nope')).toBeUndefined()
|
|
63
|
+
})
|
|
64
|
+
})
|
package/src/lib/pulumi/token.ts
CHANGED
|
@@ -8,3 +8,31 @@ export function tokenDisplayName(token: string): string {
|
|
|
8
8
|
const short = token.split(':').pop()
|
|
9
9
|
return short && short.length > 0 ? short : token
|
|
10
10
|
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A URL- and filesystem-safe form of a token, used as the `$token` route param.
|
|
14
|
+
*
|
|
15
|
+
* Tokens contain `:` and `/`, which only survive a URL as percent-escapes —
|
|
16
|
+
* and static hosts (GitHub/GitLab Pages) decode those before matching a file,
|
|
17
|
+
* so `random%3Aindex%2FrandomString%3ARandomString` would never resolve to the
|
|
18
|
+
* page generated for it. Replacing the separators keeps one URL shape that
|
|
19
|
+
* works both when served dynamically and as prerendered files on disk.
|
|
20
|
+
*/
|
|
21
|
+
export function tokenSlug(token: string): string {
|
|
22
|
+
return token.replace(/[^a-zA-Z0-9._~-]+/g, '-')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Resolves a `$token` route param back to the schema token it names. Accepts
|
|
27
|
+
* either a slug (what links produce) or the raw token, so a URL pasted from
|
|
28
|
+
* schema output still works.
|
|
29
|
+
*/
|
|
30
|
+
export function findTokenBySlug(
|
|
31
|
+
tokens: Iterable<string>,
|
|
32
|
+
slug: string,
|
|
33
|
+
): string | undefined {
|
|
34
|
+
for (const token of tokens) {
|
|
35
|
+
if (token === slug || tokenSlug(token) === slug) return token
|
|
36
|
+
}
|
|
37
|
+
return undefined
|
|
38
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* True when the app is being built as a static site (`pulumi-package-docs
|
|
3
|
+
* static`), for publishing to GitHub/GitLab Pages.
|
|
4
|
+
*
|
|
5
|
+
* A static build has no server behind it, so its pages can't call the server
|
|
6
|
+
* functions in `@/lib/pulumi/api` on the client. Every page is prerendered
|
|
7
|
+
* with its data already baked in and shipped without the hydration script:
|
|
8
|
+
* links then behave as ordinary anchors and each page load is self-contained,
|
|
9
|
+
* which also keeps a large provider's schema out of the pages that don't need
|
|
10
|
+
* it.
|
|
11
|
+
*
|
|
12
|
+
* Vite inlines `import.meta.env.VITE_*` at build time, so this folds away to a
|
|
13
|
+
* constant in both builds.
|
|
14
|
+
*/
|
|
15
|
+
export const isStaticDocs = import.meta.env.VITE_STATIC_DOCS === 'true'
|
package/src/router.tsx
CHANGED
|
@@ -4,6 +4,9 @@ import { routeTree } from './routeTree.gen'
|
|
|
4
4
|
export function getRouter() {
|
|
5
5
|
const router = createTanStackRouter({
|
|
6
6
|
routeTree,
|
|
7
|
+
// Set from Vite's `base`, so a static build published under a project
|
|
8
|
+
// path (e.g. https://<user>.github.io/<repo>/) still links correctly.
|
|
9
|
+
basepath: import.meta.env.BASE_URL,
|
|
7
10
|
scrollRestoration: true,
|
|
8
11
|
defaultPreload: 'intent',
|
|
9
12
|
defaultPreloadStaleTime: 0,
|
package/src/routes/__root.tsx
CHANGED
|
@@ -6,7 +6,8 @@ import {
|
|
|
6
6
|
} from '@tanstack/react-router'
|
|
7
7
|
|
|
8
8
|
import { PulumiLogo } from '@/components/PulumiLogo'
|
|
9
|
-
import { ThemeToggle } from '@/components/ThemeToggle'
|
|
9
|
+
import { StaticThemeToggle, ThemeToggle } from '@/components/ThemeToggle'
|
|
10
|
+
import { isStaticDocs } from '@/lib/static-docs'
|
|
10
11
|
import appCss from '../styles.css?url'
|
|
11
12
|
|
|
12
13
|
// Runs before hydration so an explicit theme choice applies before first
|
|
@@ -57,12 +58,14 @@ function RootDocument({ children }: { children: React.ReactNode }) {
|
|
|
57
58
|
<span className="site-brand-divider" aria-hidden="true" />
|
|
58
59
|
<span className="site-brand-name">Package Docs</span>
|
|
59
60
|
</Link>
|
|
60
|
-
<ThemeToggle />
|
|
61
|
+
{isStaticDocs ? <StaticThemeToggle /> : <ThemeToggle />}
|
|
61
62
|
</div>
|
|
62
63
|
</header>
|
|
63
64
|
{children}
|
|
64
65
|
|
|
65
|
-
|
|
66
|
+
{/* A static build has no server to call back into, so it ships
|
|
67
|
+
without the hydration script and navigates as plain links. */}
|
|
68
|
+
{isStaticDocs ? null : <Scripts />}
|
|
66
69
|
</body>
|
|
67
70
|
</html>
|
|
68
71
|
)
|
|
@@ -2,16 +2,19 @@ import { Link, createFileRoute, notFound } from '@tanstack/react-router'
|
|
|
2
2
|
|
|
3
3
|
import { MemberDetail } from '@/components/MemberDetail'
|
|
4
4
|
import { getProvider } from '@/lib/pulumi/api'
|
|
5
|
-
import { tokenDisplayName } from '@/lib/pulumi/token'
|
|
5
|
+
import { findTokenBySlug, tokenDisplayName } from '@/lib/pulumi/token'
|
|
6
6
|
|
|
7
7
|
export const Route = createFileRoute('/providers/$name/functions/$token')({
|
|
8
8
|
loader: async ({ params }) => {
|
|
9
9
|
const entry = await getProvider({ data: params.name })
|
|
10
|
-
const
|
|
11
|
-
|
|
10
|
+
const functions = entry?.schema?.functions
|
|
11
|
+
const token =
|
|
12
|
+
functions && findTokenBySlug(Object.keys(functions), params.token)
|
|
13
|
+
const fn = token ? functions[token] : undefined
|
|
14
|
+
if (!token || !fn) throw notFound()
|
|
12
15
|
return {
|
|
13
16
|
providerName: params.name,
|
|
14
|
-
token
|
|
17
|
+
token,
|
|
15
18
|
fn,
|
|
16
19
|
runtime: entry?.runtime,
|
|
17
20
|
}
|
|
@@ -3,7 +3,7 @@ import { Link, createFileRoute, notFound } from '@tanstack/react-router'
|
|
|
3
3
|
import { getProvider } from '@/lib/pulumi/api'
|
|
4
4
|
import { resolveDescription } from '@/lib/pulumi/description'
|
|
5
5
|
import { summarizeDescription } from '@/lib/pulumi/examples'
|
|
6
|
-
import { tokenDisplayName } from '@/lib/pulumi/token'
|
|
6
|
+
import { tokenDisplayName, tokenSlug } from '@/lib/pulumi/token'
|
|
7
7
|
import type { SchemaFunction, SchemaResource } from '@/lib/pulumi/types'
|
|
8
8
|
|
|
9
9
|
export const Route = createFileRoute('/providers/$name/')({
|
|
@@ -43,7 +43,7 @@ function MemberListItem({
|
|
|
43
43
|
|
|
44
44
|
return (
|
|
45
45
|
<li className="doc-list-item">
|
|
46
|
-
<Link to={to} params={{ name: providerName, token }}>
|
|
46
|
+
<Link to={to} params={{ name: providerName, token: tokenSlug(token) }}>
|
|
47
47
|
<code>{tokenDisplayName(token)}</code>
|
|
48
48
|
</Link>
|
|
49
49
|
{deprecated && <span className="badge badge-warning">Deprecated</span>}
|
|
@@ -2,16 +2,19 @@ import { Link, createFileRoute, notFound } from '@tanstack/react-router'
|
|
|
2
2
|
|
|
3
3
|
import { MemberDetail } from '@/components/MemberDetail'
|
|
4
4
|
import { getProvider } from '@/lib/pulumi/api'
|
|
5
|
-
import { tokenDisplayName } from '@/lib/pulumi/token'
|
|
5
|
+
import { findTokenBySlug, tokenDisplayName } from '@/lib/pulumi/token'
|
|
6
6
|
|
|
7
7
|
export const Route = createFileRoute('/providers/$name/resources/$token')({
|
|
8
8
|
loader: async ({ params }) => {
|
|
9
9
|
const entry = await getProvider({ data: params.name })
|
|
10
|
-
const
|
|
11
|
-
|
|
10
|
+
const resources = entry?.schema?.resources
|
|
11
|
+
const token =
|
|
12
|
+
resources && findTokenBySlug(Object.keys(resources), params.token)
|
|
13
|
+
const resource = token ? resources[token] : undefined
|
|
14
|
+
if (!token || !resource) throw notFound()
|
|
12
15
|
return {
|
|
13
16
|
providerName: params.name,
|
|
14
|
-
token
|
|
17
|
+
token,
|
|
15
18
|
resource,
|
|
16
19
|
runtime: entry?.runtime,
|
|
17
20
|
}
|
package/vite.config.ts
CHANGED
|
@@ -38,9 +38,49 @@ function printMcpUrl(): Plugin {
|
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
// Set by `pulumi-package-docs static` (see bin/static.js). A static build
|
|
42
|
+
// prerenders every page to HTML under `dist-static/client` so the result can
|
|
43
|
+
// be published to GitHub/GitLab Pages, where there's no server to render them.
|
|
44
|
+
const staticDocs = process.env.VITE_STATIC_DOCS === 'true'
|
|
45
|
+
// The path the site is served under, e.g. "/<repo>/" for a GitHub/GitLab
|
|
46
|
+
// Pages project site. Vite prefixes asset URLs with it, and the router picks
|
|
47
|
+
// it up as its basepath via `import.meta.env.BASE_URL`.
|
|
48
|
+
const base = process.env.PULUMI_DOCS_BASE || '/'
|
|
49
|
+
|
|
41
50
|
const config = defineConfig({
|
|
51
|
+
base,
|
|
52
|
+
// A static build goes to its own output directory, so generating a site
|
|
53
|
+
// never clobbers the `dist/` build that `pulumi-package-docs` serves.
|
|
54
|
+
...(staticDocs
|
|
55
|
+
? {
|
|
56
|
+
build: { outDir: 'dist-static' },
|
|
57
|
+
preview: { host: '127.0.0.1' },
|
|
58
|
+
}
|
|
59
|
+
: {}),
|
|
42
60
|
resolve: { tsconfigPaths: true },
|
|
43
|
-
plugins: [
|
|
61
|
+
plugins: [
|
|
62
|
+
tanstackStart(
|
|
63
|
+
staticDocs
|
|
64
|
+
? {
|
|
65
|
+
// Crawling from the index reaches every provider page and, from
|
|
66
|
+
// there, every resource/function page — so the page list follows
|
|
67
|
+
// whatever the project's Pulumi.yaml declares, with nothing to
|
|
68
|
+
// enumerate here.
|
|
69
|
+
pages: [{ path: '/' }],
|
|
70
|
+
prerender: {
|
|
71
|
+
enabled: true,
|
|
72
|
+
crawlLinks: true,
|
|
73
|
+
failOnError: true,
|
|
74
|
+
// `/mcp` is a server endpoint (Streamable HTTP), not a page;
|
|
75
|
+
// there's nothing to prerender and no server to serve it from.
|
|
76
|
+
filter: (page) => page.path !== '/mcp',
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
: undefined,
|
|
80
|
+
),
|
|
81
|
+
viteReact(),
|
|
82
|
+
printMcpUrl(),
|
|
83
|
+
],
|
|
44
84
|
})
|
|
45
85
|
|
|
46
86
|
export default config
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{o as e}from"./useSelector-BIt42tGI.js";import{o as t}from"./index-2FzxAhzN.js";import{i as n,n as r,o as i,r as a}from"./token-oQmZfAP3.js";var o=e();function s({example:e}){return e?(0,o.jsxs)(`div`,{className:`code-example`,children:[(0,o.jsx)(`span`,{className:`code-example-lang`,children:e.language}),(0,o.jsx)(`pre`,{children:(0,o.jsx)(`code`,{children:e.code})})]}):(0,o.jsx)(`p`,{className:`empty-state`,children:`No examples in the schema.`})}function c(e){if(!e)return`any`;if(e.$ref){let t=e.$ref.split(`/`).pop()??e.$ref;return decodeURIComponent(t)}return e.type===`array`&&e.items?`${c(e.items)}[]`:e.type??`any`}function l(e){return e.replace(/([a-z0-9])([A-Z])/g,`$1_$2`).replace(/([A-Z]+)([A-Z][a-z])/g,`$1_$2`).toLowerCase()}function u(e){return e.length===0?e:e.charAt(0).toUpperCase()+e.slice(1)}function d(e,t){switch(t){case`python`:return l(e);case`go`:case`dotnet`:return u(e);default:return e}}function f({properties:e,required:t,runtime:n}){let r=Object.keys(e??{});if(r.length===0)return(0,o.jsx)(`p`,{className:`empty-state`,children:`None`});let a=new Set(t??[]);return(0,o.jsxs)(`table`,{className:`props-table`,children:[(0,o.jsx)(`thead`,{children:(0,o.jsxs)(`tr`,{children:[(0,o.jsx)(`th`,{children:`Name`}),(0,o.jsx)(`th`,{children:`Type`}),(0,o.jsx)(`th`,{children:`Required`}),(0,o.jsx)(`th`,{children:`Description`})]})}),(0,o.jsx)(`tbody`,{children:r.map(t=>{let r=e[t];return(0,o.jsxs)(`tr`,{children:[(0,o.jsx)(`td`,{children:(0,o.jsx)(`code`,{children:d(t,n)})}),(0,o.jsx)(`td`,{children:(0,o.jsx)(`code`,{children:c(r)})}),(0,o.jsx)(`td`,{children:a.has(t)?`yes`:`no`}),(0,o.jsx)(`td`,{children:i(r.description,n)??``})]},t)})})]})}function p({providerName:e,kind:c,displayName:l,token:u,description:d,deprecationMessage:p,runtime:m,inputProperties:h,requiredInputs:g,outputProperties:_,requiredOutputs:v}){let y=i(d,m),b=n(y),x=a(r(y),m);return(0,o.jsxs)(`main`,{className:`provider-detail`,children:[(0,o.jsxs)(t,{to:`/providers/$name`,params:{name:e},className:`back-link`,children:[`← Back to `,e]}),(0,o.jsx)(`h1`,{children:l}),(0,o.jsxs)(`p`,{className:`subtitle`,children:[(0,o.jsx)(`span`,{className:`badge ${c===`Resource`?`badge-resource`:`badge-function`}`,children:c}),` `,`· `,(0,o.jsx)(`code`,{children:u})]}),p&&(0,o.jsxs)(`p`,{className:`banner banner-warning`,children:[`Deprecated: `,p]}),b?.split(/\n{2,}/).map((e,t)=>(0,o.jsx)(`p`,{children:e},t)),(0,o.jsx)(`h2`,{children:`Example`}),(0,o.jsx)(s,{example:x}),(0,o.jsx)(`h2`,{children:`Inputs`}),(0,o.jsx)(f,{properties:h,required:g,runtime:m}),(0,o.jsx)(`h2`,{children:`Outputs`}),(0,o.jsx)(f,{properties:_,required:v,runtime:m})]})}export{p as t};
|