Skip to content
This repository was archived by the owner on May 23, 2026. It is now read-only.

Commit 5a928eb

Browse files
Support declarative extension contributions
1 parent a3cfa2c commit 5a928eb

6 files changed

Lines changed: 209 additions & 43 deletions

File tree

CONTRIBUTING.md

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ manifests.json # Combined manifests (auto-generated, do not edit manual
2121
scripts/ # Validation and generation scripts
2222
```
2323

24-
- `extension.json` defines the extension manifest (category, capabilities, tool references)
24+
- `extension.json` defines the extension manifest (category, contributions, capabilities, tool references)
2525
- `tooling.json` (optional) defines pre-built platform-specific binaries distributed as tarballs
2626
- Not every extension has a `tooling.json`. Extensions without one rely on runtime-installed tools
2727

@@ -40,18 +40,25 @@ scripts/ # Validation and generation scripts
4040
"version": "1.0.0",
4141
"description": "MyLang language support with LSP",
4242
"publisher": "Athas",
43+
"engines": {
44+
"athas": ">=0.7.0"
45+
},
4346
"categories": ["Language"],
44-
"languages": [
45-
{
46-
"id": "mylang",
47-
"extensions": [".ml"],
48-
"aliases": ["MyLang"]
49-
}
50-
]
47+
"contributes": {
48+
"languages": [
49+
{
50+
"id": "mylang",
51+
"extensions": [".ml"],
52+
"filenames": ["MyLangfile"],
53+
"filenamePatterns": ["*.mylang.json"],
54+
"aliases": ["MyLang"]
55+
}
56+
]
57+
}
5158
}
5259
```
5360

54-
3. Add capability entries in `capabilities` only for tooling provided by the extension (`lsp`, `formatter`, `linter`, snippets/commands as needed).
61+
3. Add capability entries in `capabilities` only for tooling provided by the extension (`lsp`, `formatter`, `linter`, grammar assets as needed). Snippets, commands, themes, icon themes, agents, database providers, and keybindings should be declared under `contributes`.
5562

5663
4. Regenerate generated files:
5764
```bash
@@ -75,6 +82,38 @@ scripts/ # Validation and generation scripts
7582
| `version` | string | Semver version |
7683
| `categories` | string[] | Extension categories (`Language`, `Theme`, `Snippets`, `Keymaps`, etc.) |
7784

85+
### Contributions
86+
87+
New manifests should prefer the declarative `contributes` model:
88+
89+
```json
90+
"contributes": {
91+
"languages": [],
92+
"snippets": [],
93+
"themes": [],
94+
"iconThemes": [],
95+
"databaseProviders": [],
96+
"agents": [],
97+
"commands": [],
98+
"keybindings": []
99+
}
100+
```
101+
102+
Top-level arrays such as `languages`, `themes`, `iconThemes`, `agents`, and `databaseProviders`
103+
are still supported for existing manifests. Validation, catalog generation, theme/icon packaging,
104+
and database sidecar packaging read both forms.
105+
106+
Language contributions can match by file extension, exact filename, or filename pattern:
107+
108+
```json
109+
{
110+
"id": "jsonc",
111+
"extensions": [".jsonc"],
112+
"filenames": ["tsconfig.json", "jsconfig.json"],
113+
"filenamePatterns": ["tsconfig.*.json", "jsconfig.*.json"]
114+
}
115+
```
116+
78117
### Categories
79118

80119
- `Language` - Language tooling support (LSP, formatter, linter, snippets, commands)
@@ -101,7 +140,7 @@ scripts/ # Validation and generation scripts
101140

102141
Use `packages` for runtime-managed companion packages that must be installed beside the primary package, such as TypeScript SDK packages required by JavaScript-based language servers.
103142

104-
Supported runtimes: `bun`, `node`, `python`, `go`, `rust`, `ruby`, `binary`
143+
Supported runtimes: `bun`, `node`, `python`, `go`, `rust`, `binary`
105144

106145
#### Formatter
107146

README.md

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,10 @@ Extensions for the [Athas](https://athas.dev) editor.
55
Syntax highlighting is now bundled in Athas core by default. This repository focuses on
66
language tooling extensions (LSP, formatter, linter, snippets), plus themes and icon themes.
77

8+
Extension manifests are declarative. New manifests should prefer the
9+
`contributes` shape for editor contributions, while managed runtime tooling stays under
10+
`capabilities`. Existing top-level contribution fields are still supported.
11+
812
## Structure
913

1014
Language extensions can live under `extensions/{name}/`. Marketplace contribution
@@ -31,6 +35,46 @@ Root-level files:
3135
- `registry.json` / `index.json` - Extension registry for the marketplace
3236
- `manifests.json` - Combined manifests (auto-generated, do not edit manually)
3337

38+
## Manifest Shape
39+
40+
```json
41+
{
42+
"$schema": "https://athas.dev/schemas/extension.json",
43+
"id": "athas.mylang",
44+
"name": "MyLang",
45+
"displayName": "MyLang",
46+
"version": "1.0.0",
47+
"publisher": "Athas",
48+
"categories": ["Language"],
49+
"engines": {
50+
"athas": ">=0.7.0"
51+
},
52+
"contributes": {
53+
"languages": [
54+
{
55+
"id": "mylang",
56+
"extensions": [".ml"],
57+
"filenames": ["MyLangfile"],
58+
"filenamePatterns": ["*.mylang.json"],
59+
"aliases": ["MyLang"]
60+
}
61+
]
62+
},
63+
"capabilities": {
64+
"lsp": {
65+
"name": "mylang-language-server",
66+
"runtime": "node",
67+
"package": "mylang-language-server",
68+
"args": ["--stdio"]
69+
}
70+
}
71+
}
72+
```
73+
74+
Supported contribution arrays include `languages`, `snippets`, `themes`, `iconThemes`,
75+
`databaseProviders`, `agents`, `commands`, and `keybindings`. The validation and catalog
76+
scripts read both top-level arrays and `contributes.*`.
77+
3478
## Scripts
3579

3680
```bash

scripts/build-extensions-index.ts

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ type ExternalLanguageManifest = {
1111
categories?: string[];
1212
languages?: Array<{
1313
id: string;
14-
extensions: string[];
14+
extensions?: string[];
1515
}>;
1616
databaseProviders?: Array<{
1717
id: string;
@@ -25,6 +25,24 @@ type ExternalLanguageManifest = {
2525
iconThemes?: Array<{
2626
id: string;
2727
}>;
28+
contributes?: {
29+
languages?: Array<{
30+
id: string;
31+
extensions?: string[];
32+
}>;
33+
databaseProviders?: Array<{
34+
id: string;
35+
}>;
36+
agents?: Array<{
37+
id: string;
38+
}>;
39+
themes?: Array<{
40+
id: string;
41+
}>;
42+
iconThemes?: Array<{
43+
id: string;
44+
}>;
45+
};
2846
installation?: {
2947
size?: number;
3048
platformArch?: Record<string, { size?: number }>;
@@ -106,6 +124,16 @@ function resolveInstallSize(manifest: ExternalLanguageManifest): number | undefi
106124
return typeof size === "number" && size > 0 ? size : undefined;
107125
}
108126

127+
function getManifestContributions<K extends keyof NonNullable<ExternalLanguageManifest["contributes"]>>(
128+
manifest: ExternalLanguageManifest,
129+
key: K,
130+
): NonNullable<ExternalLanguageManifest["contributes"]>[K] {
131+
return [
132+
...(((manifest as Record<string, unknown>)[key] as unknown[]) || []),
133+
...((manifest.contributes?.[key] as unknown[]) || []),
134+
] as NonNullable<ExternalLanguageManifest["contributes"]>[K];
135+
}
136+
109137
function withTrailingNewline(json: unknown): string {
110138
return `${JSON.stringify(json, null, 2)}\n`;
111139
}
@@ -146,11 +174,11 @@ async function buildCatalog() {
146174
throw new Error(`Missing id in ${manifestPath}`);
147175
}
148176

149-
const languages = manifest.languages ?? [];
150-
const databaseProviders = manifest.databaseProviders ?? [];
151-
const agents = manifest.agents ?? [];
152-
const themes = manifest.themes ?? [];
153-
const iconThemes = manifest.iconThemes ?? [];
177+
const languages = getManifestContributions(manifest, "languages") ?? [];
178+
const databaseProviders = getManifestContributions(manifest, "databaseProviders") ?? [];
179+
const agents = getManifestContributions(manifest, "agents") ?? [];
180+
const themes = getManifestContributions(manifest, "themes") ?? [];
181+
const iconThemes = getManifestContributions(manifest, "iconThemes") ?? [];
154182
if (
155183
languages.length === 0 &&
156184
databaseProviders.length === 0 &&

scripts/package-database-sidecars.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,25 @@ function hasCompletePackageInfo(value: unknown): value is Record<string, unknown
4343
);
4444
}
4545

46+
function getContributionArray(
47+
manifest: Record<string, unknown>,
48+
key: string,
49+
): Array<Record<string, unknown>> {
50+
const contributes =
51+
typeof manifest.contributes === "object" &&
52+
manifest.contributes !== null &&
53+
!Array.isArray(manifest.contributes)
54+
? (manifest.contributes as Record<string, unknown>)
55+
: {};
56+
57+
return [
58+
...(Array.isArray(manifest[key]) ? (manifest[key] as Array<Record<string, unknown>>) : []),
59+
...(Array.isArray(contributes[key])
60+
? (contributes[key] as Array<Record<string, unknown>>)
61+
: []),
62+
];
63+
}
64+
4665
async function createPackage(params: {
4766
extensionDir: string;
4867
manifest: Record<string, unknown>;
@@ -91,7 +110,7 @@ for (const folder of databaseFolders) {
91110
const extensionDir = join(extensionsDir, "database", folder);
92111
const manifestPath = join(extensionDir, "extension.json");
93112
const manifest = JSON.parse(await readFile(manifestPath, "utf8")) as Record<string, unknown>;
94-
const provider = (manifest.databaseProviders as Array<Record<string, unknown>> | undefined)?.[0];
113+
const provider = getContributionArray(manifest, "databaseProviders")[0];
95114
const sidecar = provider?.sidecar as Record<string, string> | undefined;
96115
const sidecarPath = sidecar?.[platformArch];
97116

scripts/package-extensions.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,19 @@ const extensionsDir = join(root, "extensions");
1111
const packagesDir = join(root, "packages");
1212
const cdnBaseUrl = process.env.EXTENSIONS_CDN_BASE_URL || "https://athas.dev/extensions";
1313

14+
function contributionCount(manifest: Record<string, unknown>, key: string): number {
15+
const contributes =
16+
typeof manifest.contributes === "object" &&
17+
manifest.contributes !== null &&
18+
!Array.isArray(manifest.contributes)
19+
? (manifest.contributes as Record<string, unknown>)
20+
: {};
21+
22+
const topLevel = Array.isArray(manifest[key]) ? manifest[key].length : 0;
23+
const contributed = Array.isArray(contributes[key]) ? contributes[key].length : 0;
24+
return topLevel + contributed;
25+
}
26+
1427
async function collectExtensionFolders(directory: string, folders: string[] = []) {
1528
const entries = await readdir(directory, { withFileTypes: true });
1629

@@ -29,12 +42,10 @@ async function collectExtensionFolders(directory: string, folders: string[] = []
2942
}
3043

3144
function shouldPackage(manifest: Record<string, unknown>) {
32-
const hasNativeSidecar =
33-
Array.isArray(manifest.databaseProviders) && manifest.databaseProviders.length > 0;
34-
const isLanguage = Array.isArray(manifest.languages) && manifest.languages.length > 0;
45+
const hasNativeSidecar = contributionCount(manifest, "databaseProviders") > 0;
46+
const isLanguage = contributionCount(manifest, "languages") > 0;
3547
const isPureAssetExtension =
36-
(Array.isArray(manifest.themes) && manifest.themes.length > 0) ||
37-
(Array.isArray(manifest.iconThemes) && manifest.iconThemes.length > 0);
48+
contributionCount(manifest, "themes") > 0 || contributionCount(manifest, "iconThemes") > 0;
3849

3950
return isPureAssetExtension && !hasNativeSidecar && !isLanguage;
4051
}

0 commit comments

Comments
 (0)