Skip to content

Commit f1a111b

Browse files
CopilotIEvangelistCopilot
authored
feat: host aspire.config.json JSON Schema at versioned aspire.dev URLs (#743)
* feat: host aspire.config.json schema at versioned aspire.dev URLs - Add src/data/schemas/ with index.json and initial aspire-config.13.2.3.schema.json - Add scripts/update-schemas.ts to fetch + version schemas from microsoft/aspire - Add src/utils/cli-config-schema.ts and cli-config-schema-types.ts utility modules - Add Astro endpoints: schema.json.ts (latest) and schema/[version].json.ts (versioned) - Add tests/unit/cli-config-schema.vitest.test.ts with 10 schema integrity tests - Update package.json: add update:schemas script and test:unit:cli-config-schema Agent-Logs-Url: https://github.com/microsoft/aspire.dev/sessions/da65b293-931e-4c52-a1cb-0f9f082d8c39 Co-authored-by: IEvangelist <7679720+IEvangelist@users.noreply.github.com> * fix: address code review feedback on schema utils and versioned endpoint Agent-Logs-Url: https://github.com/microsoft/aspire.dev/sessions/da65b293-931e-4c52-a1cb-0f9f082d8c39 Co-authored-by: IEvangelist <7679720+IEvangelist@users.noreply.github.com> * feat: add JSON Schema support for aspire.config.json and update middleware for schema redirection Co-authored-by: Copilot <copilot@github.com> * feat: enhance schema handling with atomic index writes and add e2e tests for schema endpoints --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: IEvangelist <7679720+IEvangelist@users.noreply.github.com> Co-authored-by: David Pine <david.pine@microsoft.com> Co-authored-by: Copilot <copilot@github.com>
1 parent de3ccb4 commit f1a111b

12 files changed

Lines changed: 850 additions & 9 deletions

File tree

src/frontend/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,21 +29,23 @@
2929
"astro": "pnpm git-env && astro",
3030
"test": "pnpm test:unit && pnpm test:e2e",
3131
"test:all": "pnpm lint && pnpm test",
32-
"test:unit": "pnpm test:unit:contracts && pnpm test:unit:components && pnpm test:unit:docs && pnpm test:unit:api-markdown && pnpm test:unit:ts-api && pnpm test:unit:twoslash-types && pnpm test:unit:structured-data",
32+
"test:unit": "pnpm test:unit:contracts && pnpm test:unit:components && pnpm test:unit:docs && pnpm test:unit:api-markdown && pnpm test:unit:ts-api && pnpm test:unit:twoslash-types && pnpm test:unit:structured-data && pnpm test:unit:cli-config-schema",
3333
"test:unit:api-markdown": "vitest run --config vitest.config.ts tests/unit/api-markdown.vitest.test.ts",
3434
"test:unit:ts-api": "vitest run --config vitest.config.ts tests/unit/ts-api-routes.vitest.test.ts tests/unit/ts-api-search.vitest.test.ts",
3535
"test:unit:twoslash-types": "vitest run --config vitest.config.ts tests/unit/twoslash-types-generator.vitest.test.ts",
3636
"test:unit:contracts": "vitest run --config vitest.config.ts tests/unit/analytics-script-contracts.vitest.test.ts",
3737
"test:unit:components": "vitest run --config vitest.config.ts tests/unit/custom-components.vitest.test.ts tests/unit/site-tour.vitest.test.ts",
3838
"test:unit:docs": "vitest run --config vitest.config.ts tests/unit/filetree-format.vitest.test.ts",
3939
"test:unit:structured-data": "vitest run --config vitest.config.ts tests/unit/structured-data.vitest.test.ts",
40+
"test:unit:cli-config-schema": "vitest run --config vitest.config.ts tests/unit/cli-config-schema.vitest.test.ts",
4041
"test:e2e:api-markdown": "playwright test --config playwright.api-markdown.config.mjs",
4142
"test:e2e": "playwright test",
4243
"test:e2e:install": "playwright install chromium",
4344
"test:e2e:serve": "pnpm git-env && pnpm check-data && astro dev --host 127.0.0.1 --port 4321",
4445
"lint": "pnpm git-env && pnpm exec astro sync && eslint . --max-warnings 0",
4546
"format": "prettier -w --cache --plugin prettier-plugin-astro .",
4647
"update:all": "pnpm update:integrations && pnpm update:github-stats && pnpm update:samples",
48+
"update:schemas": "tsx ./scripts/update-schemas.ts",
4749
"update:integrations": "tsx ./scripts/update-integrations.ts",
4850
"update:ts-api": "tsx ./scripts/update-ts-api.ts",
4951
"update:github-stats": "tsx ./scripts/update-github-stats.ts",
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
/**
2+
* Fetches the aspire-config JSON Schema from microsoft/aspire and saves a
3+
* versioned copy under src/data/schemas/, then updates the version index.
4+
*
5+
* Usage:
6+
* pnpm update:schemas # fetch latest non-prerelease
7+
* pnpm update:schemas -- --version 13.2.3 # pin to a specific version tag
8+
*/
9+
10+
import fs from 'fs';
11+
import path from 'path';
12+
import { fileURLToPath } from 'url';
13+
14+
import { fetchWithProxy as fetch } from './fetch-with-proxy';
15+
16+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
17+
const FRONTEND_ROOT = path.resolve(__dirname, '..');
18+
19+
const ASPIRE_REPO = 'microsoft/aspire';
20+
const SCHEMA_SOURCE_PATH = 'extension/schemas/aspire-config.schema.json';
21+
const SCHEMAS_DIR = path.join(FRONTEND_ROOT, 'src', 'data', 'schemas');
22+
const INDEX_FILE = path.join(SCHEMAS_DIR, 'index.json');
23+
24+
const SITE_ORIGIN = 'https://aspire.dev';
25+
const SCHEMA_BASE_PATH = '/reference/cli/configuration/schema';
26+
27+
interface GitHubRelease {
28+
tag_name: string;
29+
prerelease: boolean;
30+
draft: boolean;
31+
}
32+
33+
interface SchemaIndex {
34+
latest: string;
35+
versions: string[];
36+
}
37+
38+
function getErrorMessage(error: unknown): string {
39+
return error instanceof Error ? error.message : String(error);
40+
}
41+
42+
function schemaFileName(version: string): string {
43+
return `aspire-config.${version}.schema.json`;
44+
}
45+
46+
function schemaFilePath(version: string): string {
47+
return path.join(SCHEMAS_DIR, schemaFileName(version));
48+
}
49+
50+
/** Fetch the latest non-prerelease, non-draft tag from microsoft/aspire. */
51+
async function fetchLatestReleaseVersion(): Promise<string> {
52+
const url = `https://api.github.com/repos/${ASPIRE_REPO}/releases`;
53+
const headers: Record<string, string> = {
54+
'User-Agent': 'aspire-schema-updater',
55+
Accept: 'application/vnd.github.v3+json',
56+
};
57+
if (process.env.GITHUB_TOKEN) {
58+
headers['Authorization'] = `token ${process.env.GITHUB_TOKEN}`;
59+
}
60+
61+
const res = await fetch(url, { headers });
62+
if (!res.ok) {
63+
throw new Error(`Failed to fetch releases: ${res.status} ${res.statusText}`);
64+
}
65+
66+
const releases = (await res.json()) as GitHubRelease[];
67+
const stable = releases.find((r) => !r.prerelease && !r.draft);
68+
if (!stable) {
69+
throw new Error('No stable release found in microsoft/aspire');
70+
}
71+
72+
// Tag names are like "v13.2.3" — strip the leading "v"
73+
return stable.tag_name.replace(/^v/, '');
74+
}
75+
76+
/** Fetch the raw schema JSON from microsoft/aspire at the given tag. */
77+
async function fetchSchemaAtTag(tag: string): Promise<Record<string, unknown>> {
78+
const rawUrl =
79+
`https://raw.githubusercontent.com/${ASPIRE_REPO}/v${tag}/${SCHEMA_SOURCE_PATH}`;
80+
const res = await fetch(rawUrl, {
81+
headers: { 'User-Agent': 'aspire-schema-updater' },
82+
});
83+
if (!res.ok) {
84+
throw new Error(`Failed to fetch schema at tag v${tag}: ${res.status} ${res.statusText}`);
85+
}
86+
return (await res.json()) as Record<string, unknown>;
87+
}
88+
89+
/** Read the current index, or return an empty one if it doesn't exist yet. */
90+
function readIndex(): SchemaIndex {
91+
if (!fs.existsSync(INDEX_FILE)) {
92+
return { latest: '', versions: [] };
93+
}
94+
return JSON.parse(fs.readFileSync(INDEX_FILE, 'utf-8')) as SchemaIndex;
95+
}
96+
97+
/** Write the index atomically by writing a temp file and renaming into place. */
98+
function writeIndex(index: SchemaIndex): void {
99+
fs.mkdirSync(SCHEMAS_DIR, { recursive: true });
100+
101+
const contents = JSON.stringify(index, null, 2) + '\n';
102+
const tempFile = path.join(
103+
SCHEMAS_DIR,
104+
`${path.basename(INDEX_FILE)}.${process.pid}.${Date.now()}.tmp`,
105+
);
106+
107+
try {
108+
fs.writeFileSync(tempFile, contents, 'utf-8');
109+
fs.renameSync(tempFile, INDEX_FILE);
110+
} catch (error) {
111+
if (fs.existsSync(tempFile)) {
112+
fs.unlinkSync(tempFile);
113+
}
114+
throw error;
115+
}
116+
}
117+
118+
async function main(): Promise<void> {
119+
// Resolve the target version
120+
const versionArg = process.argv.indexOf('--version');
121+
let version: string;
122+
if (versionArg >= 0 && process.argv[versionArg + 1]) {
123+
version = process.argv[versionArg + 1].replace(/^v/, '');
124+
console.log(`📌 Using pinned version: ${version}`);
125+
} else {
126+
console.log(`🔍 Fetching latest release from ${ASPIRE_REPO}…`);
127+
version = await fetchLatestReleaseVersion();
128+
console.log(`✅ Latest stable release: ${version}`);
129+
}
130+
131+
const outFile = schemaFilePath(version);
132+
133+
if (fs.existsSync(outFile)) {
134+
console.log(`ℹ️ Schema for v${version} already exists at ${path.relative(FRONTEND_ROOT, outFile)}`);
135+
} else {
136+
console.log(`⬇️ Fetching schema from microsoft/aspire @ v${version}…`);
137+
const schema = await fetchSchemaAtTag(version);
138+
139+
// Update $id to point to the versioned aspire.dev URL
140+
schema['$id'] = `${SITE_ORIGIN}${SCHEMA_BASE_PATH}/${version}.json`;
141+
142+
fs.mkdirSync(SCHEMAS_DIR, { recursive: true });
143+
fs.writeFileSync(outFile, JSON.stringify(schema, null, 2) + '\n', 'utf-8');
144+
console.log(`✅ Saved schema to ${path.relative(FRONTEND_ROOT, outFile)}`);
145+
}
146+
147+
// Update the index
148+
const index = readIndex();
149+
150+
if (!index.versions.includes(version)) {
151+
index.versions.push(version);
152+
index.versions.sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
153+
}
154+
index.latest = version;
155+
156+
writeIndex(index);
157+
console.log(`✅ Updated ${path.relative(FRONTEND_ROOT, INDEX_FILE)} — latest: ${version}, versions: [${index.versions.join(', ')}]`);
158+
}
159+
160+
main().catch((error: unknown) => {
161+
console.error('❌ Error:', getErrorMessage(error));
162+
process.exit(1);
163+
});

src/frontend/src/content/docs/reference/cli/configuration.mdx

Lines changed: 85 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,11 @@ focuses on the settings surfaced through `aspire config`.
2222

2323
## Example `aspire.config.json`
2424

25-
```json title="aspire.config.json"
25+
```json title="aspire.config.json" {2}
2626
{
27+
"$schema": "https://aspire.dev/reference/cli/configuration/schema.json",
2728
"appHost": {
28-
"path": "./src/MyApp.AppHost/MyApp.AppHost.csproj"
29+
"path": "./apphost.ts"
2930
},
3031
"channel": "staging",
3132
"features": {
@@ -36,6 +37,83 @@ focuses on the settings surfaced through `aspire config`.
3637
}
3738
```
3839

40+
:::tip[The highlighted `$schema` property is additive]
41+
By default, the configuration files do not include the `$schema` property. You can add it manually to enable editor support for JSON Schema.
42+
:::
43+
44+
## JSON Schema support
45+
46+
Aspire publishes a JSON Schema for `aspire.config.json` so editors can provide
47+
completions, validation, and hover documentation while you edit the file.
48+
49+
Two URLs are available:
50+
51+
- **Latest** — always points to the newest published schema:
52+
53+
```text title="Latest schema URL"
54+
https://aspire.dev/reference/cli/configuration/schema.json
55+
```
56+
57+
[`schema.json`](/reference/cli/configuration/schema.json)
58+
59+
- **Versioned** — pins to a specific Aspire release (for example, `13.2.3`):
60+
61+
```text title="Versioned schema URL"
62+
https://aspire.dev/reference/cli/configuration/schema/13.2.3.json
63+
```
64+
65+
[`schema/13.2.3.json`](/reference/cli/configuration/schema/13.2.3.json)
66+
67+
### Reference the schema from `aspire.config.json`
68+
69+
Most editors automatically pick up a schema when the JSON file declares a
70+
top-level `$schema` property:
71+
72+
```json title="aspire.config.json" ins={2}
73+
{
74+
"$schema": "https://aspire.dev/reference/cli/configuration/schema.json",
75+
"appHost": {
76+
"path": "./apphost.ts"
77+
}
78+
}
79+
```
80+
81+
:::note
82+
Use the versioned URL (for example, `.../schema/13.2.3.json`) when you want
83+
schema behavior to stay stable alongside a pinned Aspire CLI version, and use
84+
the unversioned `schema.json` when you want to always validate against the
85+
latest release.
86+
:::
87+
88+
### Associate the schema in Visual Studio Code
89+
90+
If you prefer not to commit a `$schema` property, you can associate the schema
91+
with `aspire.config.json` in your user or workspace `settings.json`:
92+
93+
```json title=".vscode/settings.json"
94+
{
95+
"json.schemas": [
96+
{
97+
"fileMatch": ["aspire.config.json"],
98+
"url": "https://aspire.dev/reference/cli/configuration/schema.json"
99+
}
100+
]
101+
}
102+
```
103+
104+
VS Code also honors the `$schema` property automatically, so either approach
105+
enables completions and validation in the built-in JSON language service.
106+
107+
### Other editors
108+
109+
Any editor or tool that understands JSON Schema Draft 2020-12 can consume the
110+
hosted schema — including JetBrains IDEs (**Settings → Languages & Frameworks →
111+
Schemas and DTDs → JSON Schema Mappings**), Neovim with
112+
[`jsonls`](https://github.com/microsoft/vscode-json-languageservice), and
113+
command-line validators such as
114+
[`ajv`](https://ajv.js.org/) or [`check-jsonschema`](https://github.com/python-jsonschema/check-jsonschema).
115+
Point the tool at either the latest or versioned URL above.
116+
39117
## Settings surfaced by `aspire config`
40118

41119
Use `aspire config list` to see the values that are currently set, and use
@@ -44,11 +122,11 @@ can configure.
44122

45123
<Include relativePath="reference/cli/includes/config-settings-table.md" />
46124

47-
<Aside type="tip">
48-
Some feature flags shown by `aspire config list --all` may correspond to
49-
preview, migration, or hidden functionality. Treat the output of your
50-
installed CLI as the authoritative source for what can be configured.
51-
</Aside>
125+
:::tip
126+
Some feature flags shown by `aspire config list --all` may correspond to
127+
preview, migration, or hidden functionality. Treat the output of your
128+
installed CLI as the authoritative source for what can be configured.
129+
:::
52130

53131
## Toggle preview feature experiences
54132

0 commit comments

Comments
 (0)