@domainlang/language 0.1.20 → 0.1.82

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.
Files changed (110) hide show
  1. package/out/domain-lang-module.d.ts +0 -2
  2. package/out/domain-lang-module.js +3 -11
  3. package/out/domain-lang-module.js.map +1 -1
  4. package/out/generated/ast.d.ts +19 -8
  5. package/out/generated/ast.js +10 -1
  6. package/out/generated/ast.js.map +1 -1
  7. package/out/generated/grammar.d.ts +1 -1
  8. package/out/generated/grammar.js +123 -28
  9. package/out/generated/grammar.js.map +1 -1
  10. package/out/generated/module.d.ts +1 -1
  11. package/out/generated/module.js +1 -1
  12. package/out/index.d.ts +0 -3
  13. package/out/index.js +0 -5
  14. package/out/index.js.map +1 -1
  15. package/out/lsp/hover/domain-lang-hover.js +4 -0
  16. package/out/lsp/hover/domain-lang-hover.js.map +1 -1
  17. package/out/sdk/index.d.ts +1 -1
  18. package/out/sdk/loader-node.d.ts +3 -7
  19. package/out/sdk/loader-node.js +9 -24
  20. package/out/sdk/loader-node.js.map +1 -1
  21. package/out/sdk/types.d.ts +21 -0
  22. package/out/services/dependency-analyzer.d.ts +39 -3
  23. package/out/services/dependency-analyzer.js +47 -22
  24. package/out/services/dependency-analyzer.js.map +1 -1
  25. package/out/services/dependency-resolver.d.ts +45 -68
  26. package/out/services/dependency-resolver.js +43 -243
  27. package/out/services/dependency-resolver.js.map +1 -1
  28. package/out/services/git-url-resolver.browser.d.ts +12 -4
  29. package/out/services/git-url-resolver.browser.js +1 -5
  30. package/out/services/git-url-resolver.browser.js.map +1 -1
  31. package/out/services/git-url-resolver.d.ts +56 -22
  32. package/out/services/git-url-resolver.js +36 -70
  33. package/out/services/git-url-resolver.js.map +1 -1
  34. package/out/services/governance-validator.d.ts +37 -1
  35. package/out/services/governance-validator.js +10 -4
  36. package/out/services/governance-validator.js.map +1 -1
  37. package/out/services/import-resolver.d.ts +6 -65
  38. package/out/services/import-resolver.js +5 -223
  39. package/out/services/import-resolver.js.map +1 -1
  40. package/out/services/performance-optimizer.d.ts +1 -1
  41. package/out/services/workspace-manager.d.ts +10 -57
  42. package/out/services/workspace-manager.js +21 -187
  43. package/out/services/workspace-manager.js.map +1 -1
  44. package/out/syntaxes/domain-lang.monarch.js +1 -1
  45. package/out/syntaxes/domain-lang.monarch.js.map +1 -1
  46. package/out/utils/import-utils.d.ts +12 -4
  47. package/out/utils/import-utils.js +135 -35
  48. package/out/utils/import-utils.js.map +1 -1
  49. package/out/validation/constants.d.ts +0 -103
  50. package/out/validation/constants.js +1 -140
  51. package/out/validation/constants.js.map +1 -1
  52. package/out/validation/domain.js +1 -46
  53. package/out/validation/domain.js.map +1 -1
  54. package/out/validation/import.d.ts +22 -46
  55. package/out/validation/import.js +85 -187
  56. package/out/validation/import.js.map +1 -1
  57. package/out/validation/maps.js +6 -10
  58. package/out/validation/maps.js.map +1 -1
  59. package/out/validation/metadata.js +1 -5
  60. package/out/validation/metadata.js.map +1 -1
  61. package/package.json +6 -8
  62. package/src/domain-lang-module.ts +6 -18
  63. package/src/domain-lang.langium +12 -7
  64. package/src/generated/ast.ts +20 -7
  65. package/src/generated/grammar.ts +123 -28
  66. package/src/generated/module.ts +1 -1
  67. package/src/index.ts +0 -7
  68. package/src/lsp/hover/domain-lang-hover.ts +2 -0
  69. package/src/sdk/index.ts +2 -0
  70. package/src/sdk/loader-node.ts +9 -29
  71. package/src/sdk/types.ts +23 -0
  72. package/src/services/dependency-analyzer.ts +84 -24
  73. package/src/services/dependency-resolver.ts +84 -301
  74. package/src/services/git-url-resolver.browser.ts +14 -9
  75. package/src/services/git-url-resolver.ts +93 -86
  76. package/src/services/governance-validator.ts +47 -5
  77. package/src/services/import-resolver.ts +8 -270
  78. package/src/services/performance-optimizer.ts +1 -1
  79. package/src/services/workspace-manager.ts +46 -237
  80. package/src/syntaxes/domain-lang.monarch.ts +1 -1
  81. package/src/utils/import-utils.ts +160 -38
  82. package/src/validation/constants.ts +1 -181
  83. package/src/validation/domain.ts +1 -54
  84. package/src/validation/import.ts +104 -228
  85. package/src/validation/maps.ts +6 -10
  86. package/src/validation/metadata.ts +1 -5
  87. package/out/lsp/domain-lang-code-actions.d.ts +0 -55
  88. package/out/lsp/domain-lang-code-actions.js +0 -143
  89. package/out/lsp/domain-lang-code-actions.js.map +0 -1
  90. package/out/lsp/domain-lang-workspace-manager.d.ts +0 -21
  91. package/out/lsp/domain-lang-workspace-manager.js +0 -93
  92. package/out/lsp/domain-lang-workspace-manager.js.map +0 -1
  93. package/out/lsp/manifest-diagnostics.d.ts +0 -82
  94. package/out/lsp/manifest-diagnostics.js +0 -230
  95. package/out/lsp/manifest-diagnostics.js.map +0 -1
  96. package/out/services/semver.d.ts +0 -98
  97. package/out/services/semver.js +0 -195
  98. package/out/services/semver.js.map +0 -1
  99. package/out/services/types.d.ts +0 -340
  100. package/out/services/types.js +0 -46
  101. package/out/services/types.js.map +0 -1
  102. package/out/validation/manifest.d.ts +0 -144
  103. package/out/validation/manifest.js +0 -327
  104. package/out/validation/manifest.js.map +0 -1
  105. package/src/lsp/domain-lang-code-actions.ts +0 -189
  106. package/src/lsp/domain-lang-workspace-manager.ts +0 -104
  107. package/src/lsp/manifest-diagnostics.ts +0 -290
  108. package/src/services/semver.ts +0 -213
  109. package/src/services/types.ts +0 -415
  110. package/src/validation/manifest.ts +0 -439
@@ -1,104 +0,0 @@
1
- import path from 'node:path';
2
- import YAML from 'yaml';
3
- import { DefaultWorkspaceManager, URI, UriUtils, type FileSystemNode, type LangiumDocument, type LangiumSharedCoreServices, type WorkspaceFolder } from 'langium';
4
- import type { CancellationToken } from 'vscode-languageserver-protocol';
5
- import { ensureImportGraphFromDocument } from '../utils/import-utils.js';
6
-
7
- /**
8
- * Langium WorkspaceManager override implementing manifest-centric import loading per PRS-010.
9
- *
10
- * Behavior:
11
- * - Skips pre-loading *.dlang during workspace scan (only entry graph is loaded when manifest exists).
12
- * - Mode A (with manifest): find nearest model.yaml in folder, load entry (default index.dlang) and its import graph.
13
- * - Mode B (no manifest): no pre-loading; imports resolved on-demand when a document is opened.
14
- * - Never performs network fetches; relies on cached dependencies/lock files. Missing cache produces diagnostics upstream.
15
- */
16
- export class DomainLangWorkspaceManager extends DefaultWorkspaceManager {
17
- constructor(services: LangiumSharedCoreServices) {
18
- super(services);
19
- }
20
-
21
- override shouldIncludeEntry(entry: FileSystemNode): boolean {
22
- // Prevent auto-including .dlang files; we'll load via entry/import graph
23
- const name = UriUtils.basename(entry.uri);
24
- if (name.toLowerCase().endsWith('.dlang')) {
25
- return false;
26
- }
27
- return super.shouldIncludeEntry(entry);
28
- }
29
-
30
- override async initializeWorkspace(folders: WorkspaceFolder[], cancelToken?: CancellationToken): Promise<void> {
31
- await super.initializeWorkspace(folders, cancelToken);
32
- }
33
-
34
- protected override async loadAdditionalDocuments(folders: WorkspaceFolder[], collector: (document: LangiumDocument) => void): Promise<void> {
35
- const manifestInfo = await this.findManifestInFolders(folders);
36
- if (!manifestInfo) {
37
- return; // Mode B: no manifest
38
- }
39
-
40
- const entryUri = URI.file(manifestInfo.entryPath);
41
- const entryDoc = await this.langiumDocuments.getOrCreateDocument(entryUri);
42
- collector(entryDoc);
43
-
44
- const uris = await ensureImportGraphFromDocument(entryDoc, this.langiumDocuments);
45
- for (const uriString of uris) {
46
- const uri = URI.parse(uriString);
47
- const doc = await this.langiumDocuments.getOrCreateDocument(uri);
48
- collector(doc);
49
- }
50
- }
51
-
52
- private async findManifestInFolders(folders: WorkspaceFolder[]): Promise<{ manifestPath: string; entryPath: string } | undefined> {
53
- for (const folder of folders) {
54
- const manifestPath = await this.findNearestManifest(folder.uri);
55
- if (manifestPath) {
56
- const entry = await this.readEntryFromManifest(manifestPath) ?? 'index.dlang';
57
- const entryPath = path.resolve(path.dirname(manifestPath), entry);
58
- return { manifestPath, entryPath };
59
- }
60
- }
61
- return undefined;
62
- }
63
-
64
- private async findNearestManifest(startUri: string): Promise<string | undefined> {
65
- let current = path.resolve(URI.parse(startUri).fsPath);
66
- const { root } = path.parse(current);
67
-
68
- while (true) {
69
- const candidate = path.join(current, 'model.yaml');
70
- if (await this.pathExists(candidate)) {
71
- return candidate;
72
- }
73
-
74
- if (current === root) {
75
- return undefined;
76
- }
77
-
78
- const parent = path.dirname(current);
79
- if (parent === current) {
80
- return undefined;
81
- }
82
- current = parent;
83
- }
84
- }
85
-
86
- private async readEntryFromManifest(manifestPath: string): Promise<string | undefined> {
87
- try {
88
- const content = await this.fileSystemProvider.readFile(URI.file(manifestPath));
89
- const manifest = (YAML.parse(content) ?? {}) as { model?: { entry?: string } };
90
- return manifest.model?.entry;
91
- } catch {
92
- return undefined;
93
- }
94
- }
95
-
96
- private async pathExists(target: string): Promise<boolean> {
97
- try {
98
- await this.fileSystemProvider.stat(URI.file(target));
99
- return true;
100
- } catch {
101
- return false;
102
- }
103
- }
104
- }
@@ -1,290 +0,0 @@
1
- /**
2
- * Manifest Diagnostics Service for DomainLang.
3
- *
4
- * Provides LSP diagnostics for model.yaml files by integrating the ManifestValidator
5
- * with the VS Code language server protocol.
6
- *
7
- * This service:
8
- * - Validates model.yaml files using ManifestValidator
9
- * - Converts ManifestDiagnostic to LSP Diagnostic format
10
- * - Sends diagnostics to the LSP connection
11
- *
12
- * @module
13
- */
14
-
15
- import type { Connection } from 'vscode-languageserver';
16
- import { Diagnostic, DiagnosticSeverity, Position, Range } from 'vscode-languageserver-types';
17
- import YAML, { type Document as YAMLDocument, type YAMLMap, type Pair, isMap, isPair, isScalar } from 'yaml';
18
- import { ManifestValidator, type ManifestDiagnostic, type ManifestSeverity } from '../validation/manifest.js';
19
- import type { ModelManifest } from '../services/types.js';
20
-
21
- /**
22
- * Service for validating model.yaml and sending diagnostics via LSP.
23
- */
24
- export class ManifestDiagnosticsService {
25
- private readonly validator = new ManifestValidator();
26
- private connection: Connection | undefined;
27
-
28
- /**
29
- * Sets the LSP connection for sending diagnostics.
30
- * Must be called before validateAndSendDiagnostics.
31
- */
32
- setConnection(connection: Connection): void {
33
- this.connection = connection;
34
- }
35
-
36
- /**
37
- * Validates a model.yaml file and sends diagnostics to the LSP connection.
38
- *
39
- * @param manifestUri - URI of the model.yaml file
40
- * @param content - Raw YAML content of the file
41
- * @param options - Validation options
42
- */
43
- async validateAndSendDiagnostics(
44
- manifestUri: string,
45
- content: string,
46
- options?: { requirePublishable?: boolean }
47
- ): Promise<void> {
48
- if (!this.connection) {
49
- return; // No connection, skip diagnostics
50
- }
51
-
52
- const diagnostics = this.validate(content, options);
53
-
54
- await this.connection.sendDiagnostics({
55
- uri: manifestUri,
56
- diagnostics
57
- });
58
- }
59
-
60
- /**
61
- * Validates manifest content and returns LSP diagnostics.
62
- *
63
- * @param content - Raw YAML content
64
- * @param options - Validation options
65
- * @returns Array of LSP diagnostics
66
- */
67
- validate(
68
- content: string,
69
- options?: { requirePublishable?: boolean }
70
- ): Diagnostic[] {
71
- // Parse YAML to get both the manifest object and source map
72
- let yamlDoc: YAMLDocument.Parsed;
73
- let manifest: ModelManifest;
74
-
75
- try {
76
- yamlDoc = YAML.parseDocument(content);
77
-
78
- // Check for YAML parse errors (they're in the errors array, not thrown)
79
- if (yamlDoc.errors && yamlDoc.errors.length > 0) {
80
- return yamlDoc.errors.map(err => ({
81
- severity: DiagnosticSeverity.Error,
82
- range: this.yamlErrorToRange(err, content),
83
- message: `YAML parse error: ${err.message}`,
84
- source: 'domainlang'
85
- }));
86
- }
87
-
88
- manifest = (yamlDoc.toJSON() ?? {}) as ModelManifest;
89
- } catch (error) {
90
- // Fallback for unexpected errors
91
- const message = error instanceof Error ? error.message : 'Invalid YAML syntax';
92
- return [{
93
- severity: DiagnosticSeverity.Error,
94
- range: Range.create(Position.create(0, 0), Position.create(0, 1)),
95
- message: `YAML parse error: ${message}`,
96
- source: 'domainlang'
97
- }];
98
- }
99
-
100
- // Run manifest validation
101
- const result = this.validator.validate(manifest, options);
102
-
103
- // Convert to LSP diagnostics with source locations
104
- return result.diagnostics.map(diag =>
105
- this.toVSCodeDiagnostic(diag, yamlDoc)
106
- );
107
- }
108
-
109
- /**
110
- * Converts a YAML error to an LSP Range.
111
- */
112
- private yamlErrorToRange(err: YAML.YAMLError, _content: string): Range {
113
- if (err.linePos && err.linePos.length >= 1) {
114
- const startPos = err.linePos[0];
115
- const startLine = startPos.line - 1; // YAML uses 1-based lines
116
- const startCol = startPos.col - 1; // YAML uses 1-based columns
117
- const endPos = err.linePos.length >= 2 ? err.linePos[1] : undefined;
118
- const endLine = endPos ? endPos.line - 1 : startLine;
119
- const endCol = endPos ? endPos.col - 1 : startCol + 1;
120
- return Range.create(
121
- Position.create(startLine, startCol),
122
- Position.create(endLine, endCol)
123
- );
124
- }
125
- return Range.create(Position.create(0, 0), Position.create(0, 1));
126
- }
127
-
128
- /**
129
- * Clears diagnostics for a manifest file.
130
- * Call this when the file is closed or deleted.
131
- */
132
- async clearDiagnostics(manifestUri: string): Promise<void> {
133
- if (!this.connection) {
134
- return;
135
- }
136
-
137
- await this.connection.sendDiagnostics({
138
- uri: manifestUri,
139
- diagnostics: []
140
- });
141
- }
142
-
143
- /**
144
- * Converts a ManifestDiagnostic to an LSP Diagnostic.
145
- */
146
- private toVSCodeDiagnostic(
147
- diag: ManifestDiagnostic,
148
- yamlDoc: YAMLDocument.Parsed
149
- ): Diagnostic {
150
- const range = this.findRangeForPath(diag.path, yamlDoc);
151
-
152
- let message = diag.message;
153
- if (diag.hint) {
154
- message += `\nHint: ${diag.hint}`;
155
- }
156
-
157
- return {
158
- severity: this.toVSCodeSeverity(diag.severity),
159
- range,
160
- message,
161
- source: 'domainlang',
162
- code: diag.code
163
- };
164
- }
165
-
166
- /**
167
- * Converts ManifestSeverity to LSP DiagnosticSeverity.
168
- */
169
- private toVSCodeSeverity(severity: ManifestSeverity): DiagnosticSeverity {
170
- switch (severity) {
171
- case 'error':
172
- return DiagnosticSeverity.Error;
173
- case 'warning':
174
- return DiagnosticSeverity.Warning;
175
- case 'info':
176
- return DiagnosticSeverity.Information;
177
- default:
178
- return DiagnosticSeverity.Warning;
179
- }
180
- }
181
-
182
- /**
183
- * Finds the source range for a YAML path like "dependencies.core.version".
184
- * Returns a fallback range at start of file if path not found.
185
- */
186
- private findRangeForPath(path: string, yamlDoc: YAMLDocument.Parsed): Range {
187
- const fallback = Range.create(Position.create(0, 0), Position.create(0, 1));
188
-
189
- if (!yamlDoc.contents || !isMap(yamlDoc.contents)) {
190
- return fallback;
191
- }
192
-
193
- const parts = path.split('.');
194
- let currentNode: unknown = yamlDoc.contents;
195
-
196
- for (const part of parts) {
197
- if (!isMap(currentNode)) {
198
- return fallback;
199
- }
200
-
201
- const mapNode = currentNode as YAMLMap;
202
- const item = mapNode.items.find((pair): pair is Pair =>
203
- isPair(pair) && isScalar(pair.key) && String(pair.key.value) === part
204
- );
205
-
206
- if (!item) {
207
- return fallback;
208
- }
209
-
210
- // If this is the last part, return the range of the key
211
- if (part === parts[parts.length - 1]) {
212
- const keyNode = item.key;
213
- if (isScalar(keyNode) && keyNode.range) {
214
- const [start, end] = keyNode.range;
215
- return this.offsetsToRange(start, end, yamlDoc.toString());
216
- }
217
- }
218
-
219
- currentNode = item.value;
220
- }
221
-
222
- return fallback;
223
- }
224
-
225
- /**
226
- * Converts byte offsets to a VS Code Range using line/column calculation.
227
- */
228
- private offsetsToRange(startOffset: number, endOffset: number, content: string): Range {
229
- const lines = content.split('\n');
230
- let currentOffset = 0;
231
- let startLine = 0;
232
- let startCol = 0;
233
- let endLine = 0;
234
- let endCol = 0;
235
- let foundStart = false;
236
- let foundEnd = false;
237
-
238
- for (let lineNum = 0; lineNum < lines.length && !foundEnd; lineNum++) {
239
- const lineLength = lines[lineNum].length + 1; // +1 for newline
240
-
241
- if (!foundStart && currentOffset + lineLength > startOffset) {
242
- startLine = lineNum;
243
- startCol = startOffset - currentOffset;
244
- foundStart = true;
245
- }
246
-
247
- if (!foundEnd && currentOffset + lineLength >= endOffset) {
248
- endLine = lineNum;
249
- endCol = endOffset - currentOffset;
250
- foundEnd = true;
251
- }
252
-
253
- currentOffset += lineLength;
254
- }
255
-
256
- return Range.create(
257
- Position.create(startLine, startCol),
258
- Position.create(endLine, endCol)
259
- );
260
- }
261
- }
262
-
263
- /**
264
- * Singleton instance for use across the language server.
265
- */
266
- let manifestDiagnosticsService: ManifestDiagnosticsService | undefined;
267
-
268
- /**
269
- * Gets or creates the manifest diagnostics service singleton.
270
- */
271
- export function getManifestDiagnosticsService(): ManifestDiagnosticsService {
272
- if (!manifestDiagnosticsService) {
273
- manifestDiagnosticsService = new ManifestDiagnosticsService();
274
- }
275
- return manifestDiagnosticsService;
276
- }
277
-
278
- /**
279
- * Helper to validate a manifest URI with the given content.
280
- * Convenience function for use in file watchers.
281
- */
282
- export async function validateManifestFile(
283
- connection: Connection,
284
- manifestUri: string,
285
- content: string
286
- ): Promise<void> {
287
- const service = getManifestDiagnosticsService();
288
- service.setConnection(connection);
289
- await service.validateAndSendDiagnostics(manifestUri, content);
290
- }
@@ -1,213 +0,0 @@
1
- /**
2
- * Semantic Versioning Utilities
3
- *
4
- * Centralized SemVer parsing, comparison, and validation for the dependency system.
5
- * All version-related logic should use these utilities to ensure consistency.
6
- *
7
- * Supported formats:
8
- * - "1.0.0" or "v1.0.0" (tags)
9
- * - "1.0.0-alpha.1" (pre-release)
10
- * - "main", "develop" (branches)
11
- * - "abc123def" (commit SHAs, 7-40 hex chars)
12
- */
13
-
14
- import type { SemVer, RefType, ParsedRef } from './types.js';
15
-
16
- // ============================================================================
17
- // Parsing
18
- // ============================================================================
19
-
20
- /**
21
- * Parses a version string into SemVer components.
22
- * Returns undefined if not a valid SemVer.
23
- *
24
- * @example
25
- * parseSemVer("v1.2.3") // { major: 1, minor: 2, patch: 3, original: "v1.2.3" }
26
- * parseSemVer("1.0.0-alpha") // { major: 1, minor: 0, patch: 0, prerelease: "alpha", ... }
27
- * parseSemVer("main") // undefined (not SemVer)
28
- */
29
- export function parseSemVer(version: string): SemVer | undefined {
30
- // Strip leading 'v' if present
31
- const normalized = version.startsWith('v') ? version.slice(1) : version;
32
-
33
- // Match semver pattern: major.minor.patch[-prerelease]
34
- const match = normalized.match(/^(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/);
35
- if (!match) return undefined;
36
-
37
- return {
38
- major: parseInt(match[1], 10),
39
- minor: parseInt(match[2], 10),
40
- patch: parseInt(match[3], 10),
41
- preRelease: match[4],
42
- original: version,
43
- };
44
- }
45
-
46
- /**
47
- * Detects the type of a git ref based on its format.
48
- *
49
- * @example
50
- * detectRefType("v1.0.0") // 'tag'
51
- * detectRefType("1.2.3") // 'tag'
52
- * detectRefType("main") // 'branch'
53
- * detectRefType("abc123def") // 'commit'
54
- */
55
- export function detectRefType(ref: string): RefType {
56
- // Commit SHA: 7-40 hex characters
57
- if (/^[0-9a-f]{7,40}$/i.test(ref)) {
58
- return 'commit';
59
- }
60
- // Tags typically start with 'v' followed by semver
61
- if (/^v?\d+\.\d+\.\d+/.test(ref)) {
62
- return 'tag';
63
- }
64
- // Everything else is treated as a branch
65
- return 'branch';
66
- }
67
-
68
- /**
69
- * Parses a ref string into a structured ParsedRef with type and optional SemVer.
70
- */
71
- export function parseRef(ref: string): ParsedRef {
72
- const type = detectRefType(ref);
73
- const semver = type === 'tag' ? parseSemVer(ref) : undefined;
74
-
75
- return { original: ref, type, semver };
76
- }
77
-
78
- // ============================================================================
79
- // Comparison
80
- // ============================================================================
81
-
82
- /**
83
- * Compares two SemVer versions.
84
- * Returns: negative if a < b, positive if a > b, zero if equal.
85
- *
86
- * @example
87
- * compareSemVer(parse("1.0.0"), parse("2.0.0")) // negative (a < b)
88
- * compareSemVer(parse("1.5.0"), parse("1.2.0")) // positive (a > b)
89
- * compareSemVer(parse("1.0.0-alpha"), parse("1.0.0")) // negative (prerelease < release)
90
- */
91
- export function compareSemVer(a: SemVer, b: SemVer): number {
92
- if (a.major !== b.major) return a.major - b.major;
93
- if (a.minor !== b.minor) return a.minor - b.minor;
94
- if (a.patch !== b.patch) return a.patch - b.patch;
95
-
96
- // Pre-release versions are lower than release versions
97
- if (a.preRelease && !b.preRelease) return -1;
98
- if (!a.preRelease && b.preRelease) return 1;
99
- if (a.preRelease && b.preRelease) {
100
- return a.preRelease.localeCompare(b.preRelease);
101
- }
102
-
103
- return 0;
104
- }
105
-
106
- /**
107
- * Picks the latest from a list of SemVer refs.
108
- * Returns the ref string (with original 'v' prefix if present).
109
- *
110
- * @example
111
- * pickLatestSemVer(["v1.0.0", "v1.5.0", "v1.2.0"]) // "v1.5.0"
112
- */
113
- export function pickLatestSemVer(refs: string[]): string | undefined {
114
- const parsed = refs
115
- .map(ref => ({ ref, semver: parseSemVer(ref) }))
116
- .filter((item): item is { ref: string; semver: SemVer } => item.semver !== undefined);
117
-
118
- if (parsed.length === 0) return undefined;
119
-
120
- parsed.sort((a, b) => compareSemVer(b.semver, a.semver)); // Descending
121
- return parsed[0].ref;
122
- }
123
-
124
- /**
125
- * Sorts version strings in descending order (newest first).
126
- * Non-SemVer refs are sorted lexicographically at the end.
127
- *
128
- * @example
129
- * sortVersionsDescending(["v1.0.0", "v2.0.0", "v1.5.0"]) // ["v2.0.0", "v1.5.0", "v1.0.0"]
130
- */
131
- export function sortVersionsDescending(versions: string[]): string[] {
132
- return [...versions].sort((a, b) => {
133
- const semverA = parseSemVer(a);
134
- const semverB = parseSemVer(b);
135
-
136
- // Both are SemVer - compare semantically
137
- if (semverA && semverB) {
138
- return compareSemVer(semverB, semverA); // Descending
139
- }
140
-
141
- // SemVer comes before non-SemVer
142
- if (semverA && !semverB) return -1;
143
- if (!semverA && semverB) return 1;
144
-
145
- // Both non-SemVer - lexicographic
146
- return b.localeCompare(a);
147
- });
148
- }
149
-
150
- // ============================================================================
151
- // Validation
152
- // ============================================================================
153
-
154
- /**
155
- * Checks if a version/ref is a pre-release.
156
- *
157
- * Pre-release identifiers: alpha, beta, rc, pre, dev, snapshot
158
- *
159
- * @example
160
- * isPreRelease("v1.0.0") // false
161
- * isPreRelease("v1.0.0-alpha") // true
162
- * isPreRelease("v1.0.0-rc.1") // true
163
- */
164
- export function isPreRelease(ref: string): boolean {
165
- const semver = parseSemVer(ref);
166
- if (semver?.preRelease) {
167
- return true;
168
- }
169
-
170
- // Also check for common pre-release patterns without proper SemVer
171
- const clean = ref.replace(/^v/, '');
172
- return /-(alpha|beta|rc|pre|dev|snapshot)/i.test(clean);
173
- }
174
-
175
- /**
176
- * Checks if two SemVer versions are compatible (same major version).
177
- *
178
- * @example
179
- * areSameMajor(parse("1.0.0"), parse("1.5.0")) // true
180
- * areSameMajor(parse("1.0.0"), parse("2.0.0")) // false
181
- */
182
- export function areSameMajor(a: SemVer, b: SemVer): boolean {
183
- return a.major === b.major;
184
- }
185
-
186
- /**
187
- * Gets the major version number from a ref string.
188
- * Returns undefined if not a valid SemVer.
189
- */
190
- export function getMajorVersion(ref: string): number | undefined {
191
- return parseSemVer(ref)?.major;
192
- }
193
-
194
- // ============================================================================
195
- // Filtering
196
- // ============================================================================
197
-
198
- /**
199
- * Filters refs to only stable versions (excludes pre-releases).
200
- *
201
- * @example
202
- * filterStableVersions(["v1.0.0", "v1.1.0-alpha", "v1.2.0"]) // ["v1.0.0", "v1.2.0"]
203
- */
204
- export function filterStableVersions(refs: string[]): string[] {
205
- return refs.filter(ref => !isPreRelease(ref));
206
- }
207
-
208
- /**
209
- * Filters refs to only SemVer tags (excludes branches and commits).
210
- */
211
- export function filterSemVerTags(refs: string[]): string[] {
212
- return refs.filter(ref => detectRefType(ref) === 'tag' && parseSemVer(ref) !== undefined);
213
- }