@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.
- package/out/domain-lang-module.d.ts +0 -2
- package/out/domain-lang-module.js +3 -11
- package/out/domain-lang-module.js.map +1 -1
- package/out/generated/ast.d.ts +19 -8
- package/out/generated/ast.js +10 -1
- package/out/generated/ast.js.map +1 -1
- package/out/generated/grammar.d.ts +1 -1
- package/out/generated/grammar.js +123 -28
- package/out/generated/grammar.js.map +1 -1
- package/out/generated/module.d.ts +1 -1
- package/out/generated/module.js +1 -1
- package/out/index.d.ts +0 -3
- package/out/index.js +0 -5
- package/out/index.js.map +1 -1
- package/out/lsp/hover/domain-lang-hover.js +4 -0
- package/out/lsp/hover/domain-lang-hover.js.map +1 -1
- package/out/sdk/index.d.ts +1 -1
- package/out/sdk/loader-node.d.ts +3 -7
- package/out/sdk/loader-node.js +9 -24
- package/out/sdk/loader-node.js.map +1 -1
- package/out/sdk/types.d.ts +21 -0
- package/out/services/dependency-analyzer.d.ts +39 -3
- package/out/services/dependency-analyzer.js +47 -22
- package/out/services/dependency-analyzer.js.map +1 -1
- package/out/services/dependency-resolver.d.ts +45 -68
- package/out/services/dependency-resolver.js +43 -243
- package/out/services/dependency-resolver.js.map +1 -1
- package/out/services/git-url-resolver.browser.d.ts +12 -4
- package/out/services/git-url-resolver.browser.js +1 -5
- package/out/services/git-url-resolver.browser.js.map +1 -1
- package/out/services/git-url-resolver.d.ts +56 -22
- package/out/services/git-url-resolver.js +36 -70
- package/out/services/git-url-resolver.js.map +1 -1
- package/out/services/governance-validator.d.ts +37 -1
- package/out/services/governance-validator.js +10 -4
- package/out/services/governance-validator.js.map +1 -1
- package/out/services/import-resolver.d.ts +6 -65
- package/out/services/import-resolver.js +5 -223
- package/out/services/import-resolver.js.map +1 -1
- package/out/services/performance-optimizer.d.ts +1 -1
- package/out/services/workspace-manager.d.ts +10 -57
- package/out/services/workspace-manager.js +21 -187
- package/out/services/workspace-manager.js.map +1 -1
- package/out/syntaxes/domain-lang.monarch.js +1 -1
- package/out/syntaxes/domain-lang.monarch.js.map +1 -1
- package/out/utils/import-utils.d.ts +12 -4
- package/out/utils/import-utils.js +135 -35
- package/out/utils/import-utils.js.map +1 -1
- package/out/validation/constants.d.ts +0 -103
- package/out/validation/constants.js +1 -140
- package/out/validation/constants.js.map +1 -1
- package/out/validation/domain.js +1 -46
- package/out/validation/domain.js.map +1 -1
- package/out/validation/import.d.ts +22 -46
- package/out/validation/import.js +85 -187
- package/out/validation/import.js.map +1 -1
- package/out/validation/maps.js +6 -10
- package/out/validation/maps.js.map +1 -1
- package/out/validation/metadata.js +1 -5
- package/out/validation/metadata.js.map +1 -1
- package/package.json +6 -8
- package/src/domain-lang-module.ts +6 -18
- package/src/domain-lang.langium +12 -7
- package/src/generated/ast.ts +20 -7
- package/src/generated/grammar.ts +123 -28
- package/src/generated/module.ts +1 -1
- package/src/index.ts +0 -7
- package/src/lsp/hover/domain-lang-hover.ts +2 -0
- package/src/sdk/index.ts +2 -0
- package/src/sdk/loader-node.ts +9 -29
- package/src/sdk/types.ts +23 -0
- package/src/services/dependency-analyzer.ts +84 -24
- package/src/services/dependency-resolver.ts +84 -301
- package/src/services/git-url-resolver.browser.ts +14 -9
- package/src/services/git-url-resolver.ts +93 -86
- package/src/services/governance-validator.ts +47 -5
- package/src/services/import-resolver.ts +8 -270
- package/src/services/performance-optimizer.ts +1 -1
- package/src/services/workspace-manager.ts +46 -237
- package/src/syntaxes/domain-lang.monarch.ts +1 -1
- package/src/utils/import-utils.ts +160 -38
- package/src/validation/constants.ts +1 -181
- package/src/validation/domain.ts +1 -54
- package/src/validation/import.ts +104 -228
- package/src/validation/maps.ts +6 -10
- package/src/validation/metadata.ts +1 -5
- package/out/lsp/domain-lang-code-actions.d.ts +0 -55
- package/out/lsp/domain-lang-code-actions.js +0 -143
- package/out/lsp/domain-lang-code-actions.js.map +0 -1
- package/out/lsp/domain-lang-workspace-manager.d.ts +0 -21
- package/out/lsp/domain-lang-workspace-manager.js +0 -93
- package/out/lsp/domain-lang-workspace-manager.js.map +0 -1
- package/out/lsp/manifest-diagnostics.d.ts +0 -82
- package/out/lsp/manifest-diagnostics.js +0 -230
- package/out/lsp/manifest-diagnostics.js.map +0 -1
- package/out/services/semver.d.ts +0 -98
- package/out/services/semver.js +0 -195
- package/out/services/semver.js.map +0 -1
- package/out/services/types.d.ts +0 -340
- package/out/services/types.js +0 -46
- package/out/services/types.js.map +0 -1
- package/out/validation/manifest.d.ts +0 -144
- package/out/validation/manifest.js +0 -327
- package/out/validation/manifest.js.map +0 -1
- package/src/lsp/domain-lang-code-actions.ts +0 -189
- package/src/lsp/domain-lang-workspace-manager.ts +0 -104
- package/src/lsp/manifest-diagnostics.ts +0 -290
- package/src/services/semver.ts +0 -213
- package/src/services/types.ts +0 -415
- package/src/validation/manifest.ts +0 -439
|
@@ -1,274 +1,24 @@
|
|
|
1
|
-
import fs from 'node:fs/promises';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { URI, type LangiumDocument } from 'langium';
|
|
4
1
|
import { WorkspaceManager } from './workspace-manager.js';
|
|
2
|
+
import { URI } from 'langium';
|
|
5
3
|
import type { DomainLangServices } from '../domain-lang-module.js';
|
|
6
|
-
import type { LockFile } from './
|
|
4
|
+
import type { LockFile } from './git-url-resolver.js';
|
|
7
5
|
|
|
8
6
|
/**
|
|
9
|
-
* ImportResolver resolves import statements using
|
|
10
|
-
*
|
|
11
|
-
* Import Types (PRS-010):
|
|
12
|
-
* - Local relative: ./path, ../path → Directory-first resolution
|
|
13
|
-
* - Path aliases: @/path, @alias/path → Configurable in model.yaml paths section
|
|
14
|
-
* - External: owner/package → Manifest dependencies
|
|
15
|
-
*
|
|
16
|
-
* Directory-First Resolution:
|
|
17
|
-
* - ./types → ./types/index.dlang → ./types.dlang
|
|
18
|
-
* - Module entry defaults to index.dlang (no model.yaml required)
|
|
7
|
+
* ImportResolver resolves import statements using WorkspaceManager and GitUrlResolver.
|
|
19
8
|
*/
|
|
20
9
|
export class ImportResolver {
|
|
21
|
-
private
|
|
10
|
+
private workspaceManager: WorkspaceManager;
|
|
22
11
|
|
|
23
12
|
constructor(services: DomainLangServices) {
|
|
24
13
|
this.workspaceManager = services.imports.WorkspaceManager;
|
|
25
14
|
}
|
|
26
15
|
|
|
27
16
|
/**
|
|
28
|
-
* Resolve an import
|
|
17
|
+
* Resolve an import URL to a file URI using the workspace's GitUrlResolver.
|
|
29
18
|
*/
|
|
30
|
-
async
|
|
31
|
-
const
|
|
32
|
-
return
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Resolve an import specifier from a base directory (non-LSP contexts).
|
|
37
|
-
*/
|
|
38
|
-
async resolveFrom(baseDir: string, specifier: string): Promise<URI> {
|
|
39
|
-
await this.workspaceManager.initialize(baseDir);
|
|
40
|
-
|
|
41
|
-
// Local relative paths (./path or ../path) - directory-first resolution
|
|
42
|
-
if (specifier.startsWith('./') || specifier.startsWith('../')) {
|
|
43
|
-
const resolved = path.resolve(baseDir, specifier);
|
|
44
|
-
return this.resolveLocalPath(resolved, specifier);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
// Path aliases (@/path or @alias/path)
|
|
48
|
-
if (specifier.startsWith('@')) {
|
|
49
|
-
return this.resolvePathAlias(specifier);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// External dependency via manifest (owner/package format)
|
|
53
|
-
return this.resolveExternalDependency(specifier);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Resolves a path alias import.
|
|
58
|
-
*
|
|
59
|
-
* @param specifier - Import specifier starting with @ (e.g., "@/lib", "@shared/types")
|
|
60
|
-
*/
|
|
61
|
-
private async resolvePathAlias(specifier: string): Promise<URI> {
|
|
62
|
-
const aliases = await this.workspaceManager.getPathAliases();
|
|
63
|
-
const root = this.workspaceManager.getWorkspaceRoot();
|
|
64
|
-
|
|
65
|
-
// Find matching alias
|
|
66
|
-
const aliasMatch = this.findMatchingAlias(specifier, aliases);
|
|
67
|
-
|
|
68
|
-
if (aliasMatch) {
|
|
69
|
-
const { alias: _alias, targetPath, remainder } = aliasMatch;
|
|
70
|
-
const manifestPath = await this.workspaceManager.getManifestPath();
|
|
71
|
-
const manifestDir = manifestPath ? path.dirname(manifestPath) : root;
|
|
72
|
-
const resolvedBase = path.resolve(manifestDir, targetPath);
|
|
73
|
-
const resolved = remainder ? path.join(resolvedBase, remainder) : resolvedBase;
|
|
74
|
-
return this.resolveLocalPath(resolved, specifier);
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
// Default: @/ maps to workspace root (implicit)
|
|
78
|
-
if (specifier.startsWith('@/')) {
|
|
79
|
-
const relativePath = specifier.slice(2);
|
|
80
|
-
const resolved = path.join(root, relativePath);
|
|
81
|
-
return this.resolveLocalPath(resolved, specifier);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
throw new Error(
|
|
85
|
-
`Unknown path alias '${specifier.split('/')[0]}' in import '${specifier}'.\n` +
|
|
86
|
-
`Hint: Define it in model.yaml paths section:\n` +
|
|
87
|
-
` paths:\n` +
|
|
88
|
-
` "${specifier.split('/')[0]}": "./some/path"`
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Finds the longest matching alias for a specifier.
|
|
94
|
-
*/
|
|
95
|
-
private findMatchingAlias(
|
|
96
|
-
specifier: string,
|
|
97
|
-
aliases: Record<string, string> | undefined
|
|
98
|
-
): { alias: string; targetPath: string; remainder: string } | undefined {
|
|
99
|
-
if (!aliases) {
|
|
100
|
-
return undefined;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Sort by length descending to match most specific alias first
|
|
104
|
-
const sortedAliases = Object.entries(aliases)
|
|
105
|
-
.sort(([a], [b]) => b.length - a.length);
|
|
106
|
-
|
|
107
|
-
for (const [alias, targetPath] of sortedAliases) {
|
|
108
|
-
// Exact match
|
|
109
|
-
if (specifier === alias) {
|
|
110
|
-
return { alias, targetPath, remainder: '' };
|
|
111
|
-
}
|
|
112
|
-
// Prefix match (alias + /)
|
|
113
|
-
if (specifier.startsWith(`${alias}/`)) {
|
|
114
|
-
return { alias, targetPath, remainder: specifier.slice(alias.length + 1) };
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
return undefined;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
/**
|
|
122
|
-
* Resolves an external dependency via manifest.
|
|
123
|
-
*
|
|
124
|
-
* NEW FORMAT (PRS-010): Import specifier is owner/package format.
|
|
125
|
-
*/
|
|
126
|
-
private async resolveExternalDependency(specifier: string): Promise<URI> {
|
|
127
|
-
const manifest = await this.workspaceManager.getManifest();
|
|
128
|
-
if (!manifest) {
|
|
129
|
-
throw new Error(
|
|
130
|
-
`External dependency '${specifier}' requires model.yaml.\n` +
|
|
131
|
-
`Hint: Create model.yaml and add the dependency:\n` +
|
|
132
|
-
` dependencies:\n` +
|
|
133
|
-
` ${specifier}:\n` +
|
|
134
|
-
` ref: v1.0.0`
|
|
135
|
-
);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
const lock = await this.workspaceManager.getLockFile();
|
|
139
|
-
if (!lock) {
|
|
140
|
-
throw new Error(
|
|
141
|
-
`Dependency '${specifier}' not installed.\n` +
|
|
142
|
-
`Hint: Run 'dlang install' to fetch dependencies and generate model.lock.`
|
|
143
|
-
);
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
const mapped = await this.workspaceManager.resolveDependencyImport(specifier);
|
|
147
|
-
if (!mapped) {
|
|
148
|
-
throw new Error(
|
|
149
|
-
`Dependency '${specifier}' not found in model.yaml.\n` +
|
|
150
|
-
`Hint: Add it to your dependencies:\n` +
|
|
151
|
-
` dependencies:\n` +
|
|
152
|
-
` ${specifier}:\n` +
|
|
153
|
-
` ref: v1.0.0`
|
|
154
|
-
);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const git = await this.workspaceManager.getGitResolver();
|
|
158
|
-
return git.resolve(mapped, { allowNetwork: false });
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
/**
|
|
162
|
-
* Resolves a local path using directory-first resolution.
|
|
163
|
-
*
|
|
164
|
-
* Per PRS-010 (updated design):
|
|
165
|
-
* - If path ends with .dlang → direct file import
|
|
166
|
-
* - If no extension → directory-first:
|
|
167
|
-
* 1. Try ./path/index.dlang (module default, no model.yaml required)
|
|
168
|
-
* 2. Try ./path.dlang (file fallback)
|
|
169
|
-
*/
|
|
170
|
-
private async resolveLocalPath(resolved: string, original: string): Promise<URI> {
|
|
171
|
-
const ext = path.extname(resolved);
|
|
172
|
-
|
|
173
|
-
if (ext === '.dlang') {
|
|
174
|
-
// Direct file import
|
|
175
|
-
await assertFileExists(resolved, original);
|
|
176
|
-
return URI.file(resolved);
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
if (ext && ext !== '.dlang') {
|
|
180
|
-
throw new Error(
|
|
181
|
-
`Invalid file extension '${ext}' in import '${original}'.\n` +
|
|
182
|
-
`Hint: DomainLang files must use the .dlang extension.`
|
|
183
|
-
);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
// No extension → directory-first resolution
|
|
187
|
-
return this.resolveDirectoryFirst(resolved, original);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Directory-first resolution: ./types → ./types/index.dlang → ./types.dlang
|
|
192
|
-
*
|
|
193
|
-
* Module entry defaults to index.dlang without requiring model.yaml.
|
|
194
|
-
* If the directory has model.yaml with custom entry, use that.
|
|
195
|
-
*/
|
|
196
|
-
private async resolveDirectoryFirst(resolved: string, original: string): Promise<URI> {
|
|
197
|
-
// Step 1: Check if directory exists with index.dlang (or custom entry)
|
|
198
|
-
const isDirectory = await this.isDirectory(resolved);
|
|
199
|
-
if (isDirectory) {
|
|
200
|
-
// Check for model.yaml to get custom entry point
|
|
201
|
-
const moduleManifestPath = path.join(resolved, 'model.yaml');
|
|
202
|
-
const entryPoint = await this.readModuleEntry(moduleManifestPath);
|
|
203
|
-
const entryFile = path.join(resolved, entryPoint);
|
|
204
|
-
|
|
205
|
-
if (await this.fileExists(entryFile)) {
|
|
206
|
-
return URI.file(entryFile);
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// Directory exists but no entry file
|
|
210
|
-
throw new Error(
|
|
211
|
-
`Module '${original}' is missing its entry file.\n` +
|
|
212
|
-
`Expected: ${resolved}/${entryPoint}\n` +
|
|
213
|
-
`Hint: Create '${entryPoint}' in the module directory, or specify a custom entry in model.yaml:\n` +
|
|
214
|
-
` model:\n` +
|
|
215
|
-
` entry: main.dlang`
|
|
216
|
-
);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Step 2: Try .dlang file fallback
|
|
220
|
-
const fileWithExt = `${resolved}.dlang`;
|
|
221
|
-
if (await this.fileExists(fileWithExt)) {
|
|
222
|
-
return URI.file(fileWithExt);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Neither directory nor file found
|
|
226
|
-
throw new Error(
|
|
227
|
-
`Cannot resolve import '${original}'.\n` +
|
|
228
|
-
`Tried:\n` +
|
|
229
|
-
` • ${resolved}/index.dlang (directory module)\n` +
|
|
230
|
-
` • ${resolved}.dlang (file)\n` +
|
|
231
|
-
`Hint: Check that the path is correct and the file exists.`
|
|
232
|
-
);
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Reads the entry point from a module's model.yaml.
|
|
237
|
-
* Defaults to index.dlang if no manifest or no entry specified.
|
|
238
|
-
*/
|
|
239
|
-
private async readModuleEntry(manifestPath: string): Promise<string> {
|
|
240
|
-
try {
|
|
241
|
-
const content = await fs.readFile(manifestPath, 'utf-8');
|
|
242
|
-
const YAML = await import('yaml');
|
|
243
|
-
const manifest = YAML.parse(content) as { model?: { entry?: string } };
|
|
244
|
-
return manifest?.model?.entry ?? 'index.dlang';
|
|
245
|
-
} catch {
|
|
246
|
-
return 'index.dlang';
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
/**
|
|
251
|
-
* Checks if a path is a directory.
|
|
252
|
-
*/
|
|
253
|
-
private async isDirectory(targetPath: string): Promise<boolean> {
|
|
254
|
-
try {
|
|
255
|
-
const stat = await fs.stat(targetPath);
|
|
256
|
-
return stat.isDirectory();
|
|
257
|
-
} catch {
|
|
258
|
-
return false;
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Checks if a file exists.
|
|
264
|
-
*/
|
|
265
|
-
private async fileExists(filePath: string): Promise<boolean> {
|
|
266
|
-
try {
|
|
267
|
-
await fs.access(filePath);
|
|
268
|
-
return true;
|
|
269
|
-
} catch {
|
|
270
|
-
return false;
|
|
271
|
-
}
|
|
19
|
+
async resolveImport(importUrl: string): Promise<URI> {
|
|
20
|
+
const gitResolver = await this.workspaceManager.getGitResolver();
|
|
21
|
+
return gitResolver.resolve(importUrl);
|
|
272
22
|
}
|
|
273
23
|
|
|
274
24
|
/**
|
|
@@ -278,15 +28,3 @@ export class ImportResolver {
|
|
|
278
28
|
return this.workspaceManager.getLockFile();
|
|
279
29
|
}
|
|
280
30
|
}
|
|
281
|
-
|
|
282
|
-
async function assertFileExists(filePath: string, original: string): Promise<void> {
|
|
283
|
-
try {
|
|
284
|
-
await fs.access(filePath);
|
|
285
|
-
} catch {
|
|
286
|
-
throw new Error(
|
|
287
|
-
`Import file not found: '${original}'.\\n` +
|
|
288
|
-
`Resolved path: ${filePath}\\n` +
|
|
289
|
-
`Hint: Check that the file exists and the path is correct.`
|
|
290
|
-
);
|
|
291
|
-
}
|
|
292
|
-
}
|
|
@@ -4,15 +4,7 @@ import YAML from 'yaml';
|
|
|
4
4
|
import { DependencyResolver } from './dependency-resolver.js';
|
|
5
5
|
import { GitUrlResolver } from './git-url-resolver.js';
|
|
6
6
|
import { getGlobalOptimizer } from './performance-optimizer.js';
|
|
7
|
-
import type {
|
|
8
|
-
LockFile,
|
|
9
|
-
LockedDependency,
|
|
10
|
-
ModelManifest,
|
|
11
|
-
DependencySpec,
|
|
12
|
-
ExtendedDependencySpec,
|
|
13
|
-
PathAliases,
|
|
14
|
-
WorkspaceManagerOptions
|
|
15
|
-
} from './types.js';
|
|
7
|
+
import type { LockFile, LockedDependency } from './git-url-resolver.js';
|
|
16
8
|
|
|
17
9
|
const DEFAULT_MANIFEST_FILES = [
|
|
18
10
|
'model.yaml'
|
|
@@ -24,6 +16,27 @@ const DEFAULT_LOCK_FILES = [
|
|
|
24
16
|
|
|
25
17
|
const JSON_SPACE = 2;
|
|
26
18
|
|
|
19
|
+
export interface WorkspaceManagerOptions {
|
|
20
|
+
readonly autoResolve?: boolean;
|
|
21
|
+
readonly manifestFiles?: readonly string[];
|
|
22
|
+
readonly lockFiles?: readonly string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface ManifestDependency {
|
|
26
|
+
readonly source?: string;
|
|
27
|
+
readonly version?: string;
|
|
28
|
+
readonly description?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface ModelManifest {
|
|
32
|
+
readonly model?: {
|
|
33
|
+
readonly name?: string;
|
|
34
|
+
readonly version?: string;
|
|
35
|
+
readonly entry?: string;
|
|
36
|
+
};
|
|
37
|
+
readonly dependencies?: Record<string, ManifestDependency>;
|
|
38
|
+
}
|
|
39
|
+
|
|
27
40
|
interface ManifestCache {
|
|
28
41
|
readonly manifest: ModelManifest;
|
|
29
42
|
readonly path: string;
|
|
@@ -95,15 +108,6 @@ export class WorkspaceManager {
|
|
|
95
108
|
return undefined;
|
|
96
109
|
}
|
|
97
110
|
|
|
98
|
-
/**
|
|
99
|
-
* Returns the parsed manifest when present, otherwise undefined.
|
|
100
|
-
* Uses cached contents when unchanged on disk.
|
|
101
|
-
*/
|
|
102
|
-
async getManifest(): Promise<ModelManifest | undefined> {
|
|
103
|
-
await this.ensureInitialized();
|
|
104
|
-
return this.loadManifest();
|
|
105
|
-
}
|
|
106
|
-
|
|
107
111
|
/**
|
|
108
112
|
* Returns the cached lock file or triggers resolution when missing.
|
|
109
113
|
*/
|
|
@@ -118,21 +122,12 @@ export class WorkspaceManager {
|
|
|
118
122
|
if (cached) {
|
|
119
123
|
this.lockFile = cached;
|
|
120
124
|
} else {
|
|
121
|
-
if (this.options.allowNetwork === false) {
|
|
122
|
-
throw new Error(
|
|
123
|
-
'Lock file (model.lock) not found and network access is disabled.\n' +
|
|
124
|
-
'Hint: Run \'dlang install\' to generate the lock file.'
|
|
125
|
-
);
|
|
126
|
-
}
|
|
127
125
|
await this.generateLockFile();
|
|
128
126
|
}
|
|
129
127
|
}
|
|
130
128
|
|
|
131
129
|
if (!this.lockFile) {
|
|
132
|
-
throw new Error(
|
|
133
|
-
'Unable to resolve workspace lock file.\n' +
|
|
134
|
-
'Hint: Ensure model.yaml exists and run \'dlang install\' to generate model.lock.'
|
|
135
|
-
);
|
|
130
|
+
throw new Error('Unable to resolve workspace lock file.');
|
|
136
131
|
}
|
|
137
132
|
|
|
138
133
|
return this.lockFile;
|
|
@@ -156,41 +151,6 @@ export class WorkspaceManager {
|
|
|
156
151
|
return this.lockFile;
|
|
157
152
|
}
|
|
158
153
|
|
|
159
|
-
/**
|
|
160
|
-
* Invalidates all cached data (manifest and lock file).
|
|
161
|
-
* Call this when config files change externally (e.g., from CLI commands).
|
|
162
|
-
*
|
|
163
|
-
* After invalidation, the next call to getManifest() or getLockFile()
|
|
164
|
-
* will re-read from disk.
|
|
165
|
-
*/
|
|
166
|
-
invalidateCache(): void {
|
|
167
|
-
this.manifestCache = undefined;
|
|
168
|
-
this.lockFile = undefined;
|
|
169
|
-
// Re-apply undefined to git resolver to clear its lock file
|
|
170
|
-
if (this.gitResolver) {
|
|
171
|
-
this.gitResolver.setLockFile(undefined);
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Invalidates only the manifest cache.
|
|
177
|
-
* Call this when model.yaml changes.
|
|
178
|
-
*/
|
|
179
|
-
invalidateManifestCache(): void {
|
|
180
|
-
this.manifestCache = undefined;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/**
|
|
184
|
-
* Invalidates only the lock file cache.
|
|
185
|
-
* Call this when model.lock changes.
|
|
186
|
-
*/
|
|
187
|
-
invalidateLockCache(): void {
|
|
188
|
-
this.lockFile = undefined;
|
|
189
|
-
if (this.gitResolver) {
|
|
190
|
-
this.gitResolver.setLockFile(undefined);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
|
|
194
154
|
/**
|
|
195
155
|
* Provides the shared git URL resolver configured with the current lock file.
|
|
196
156
|
*/
|
|
@@ -215,44 +175,12 @@ export class WorkspaceManager {
|
|
|
215
175
|
}
|
|
216
176
|
|
|
217
177
|
/**
|
|
218
|
-
*
|
|
178
|
+
* Resolves a manifest dependency alias to its git import string.
|
|
179
|
+
*
|
|
180
|
+
* @param aliasPath - Alias from import statement (may include subpaths)
|
|
181
|
+
* @returns Resolved git import string or undefined when alias is unknown
|
|
219
182
|
*/
|
|
220
|
-
async
|
|
221
|
-
const manifest = await this.getManifest();
|
|
222
|
-
return manifest?.paths;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
/**
|
|
226
|
-
* Normalizes a dependency entry to its extended form.
|
|
227
|
-
* Handles both short form (string version) and extended form (object).
|
|
228
|
-
*
|
|
229
|
-
* In the new format, the key IS the owner/package, so source is derived from key
|
|
230
|
-
* ONLY for git dependencies (not for path-based local dependencies).
|
|
231
|
-
*/
|
|
232
|
-
private normalizeDependency(key: string, dep: DependencySpec): ExtendedDependencySpec {
|
|
233
|
-
if (typeof dep === 'string') {
|
|
234
|
-
// Short form: "owner/package": "v1.0.0" or "main"
|
|
235
|
-
// Key is the source (owner/package format)
|
|
236
|
-
return { source: key, ref: dep };
|
|
237
|
-
}
|
|
238
|
-
// Extended form:
|
|
239
|
-
// - If has source: use as-is
|
|
240
|
-
// - If has path: it's a local dep, don't set source
|
|
241
|
-
// - If neither: derive source from key (owner/package becomes source)
|
|
242
|
-
if (dep.source || dep.path) {
|
|
243
|
-
return dep;
|
|
244
|
-
}
|
|
245
|
-
return { ...dep, source: key };
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Resolves a manifest dependency to its git import string.
|
|
250
|
-
*
|
|
251
|
-
* NEW FORMAT (PRS-010): Dependencies are keyed by owner/package directly
|
|
252
|
-
* @param specifier - Import specifier (owner/package format, may include subpaths)
|
|
253
|
-
* @returns Resolved git import string or undefined when not found
|
|
254
|
-
*/
|
|
255
|
-
async resolveDependencyImport(specifier: string): Promise<string | undefined> {
|
|
183
|
+
async resolveDependencyImport(aliasPath: string): Promise<string | undefined> {
|
|
256
184
|
await this.ensureInitialized();
|
|
257
185
|
const manifest = await this.loadManifest();
|
|
258
186
|
const dependencies = manifest?.dependencies;
|
|
@@ -261,28 +189,18 @@ export class WorkspaceManager {
|
|
|
261
189
|
return undefined;
|
|
262
190
|
}
|
|
263
191
|
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
for (const [key, dep] of Object.entries(dependencies)) {
|
|
267
|
-
const normalized = this.normalizeDependency(key, dep);
|
|
268
|
-
|
|
269
|
-
// Skip path-based dependencies (handled by path aliases)
|
|
270
|
-
if (normalized.path) {
|
|
271
|
-
continue;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
if (!normalized.source) {
|
|
192
|
+
for (const [alias, dep] of Object.entries(dependencies)) {
|
|
193
|
+
if (!dep?.source) {
|
|
275
194
|
continue;
|
|
276
195
|
}
|
|
277
196
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
const
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
? (ref.startsWith('@') ? ref : `@${ref}`)
|
|
197
|
+
if (aliasPath === alias || aliasPath.startsWith(`${alias}/`)) {
|
|
198
|
+
const suffix = aliasPath.slice(alias.length);
|
|
199
|
+
const version = dep.version ?? '';
|
|
200
|
+
const versionSegment = version
|
|
201
|
+
? (version.startsWith('@') ? version : `@${version}`)
|
|
284
202
|
: '';
|
|
285
|
-
return `${
|
|
203
|
+
return `${dep.source}${versionSegment}${suffix}`;
|
|
286
204
|
}
|
|
287
205
|
}
|
|
288
206
|
|
|
@@ -290,15 +208,16 @@ export class WorkspaceManager {
|
|
|
290
208
|
}
|
|
291
209
|
|
|
292
210
|
private async performInitialization(startPath: string): Promise<void> {
|
|
293
|
-
this.workspaceRoot = await this.findWorkspaceRoot(startPath)
|
|
211
|
+
this.workspaceRoot = await this.findWorkspaceRoot(startPath);
|
|
212
|
+
if (!this.workspaceRoot) {
|
|
213
|
+
throw new Error('Workspace root (directory with model.yaml) not found.');
|
|
214
|
+
}
|
|
294
215
|
|
|
295
|
-
|
|
296
|
-
const cacheDir = path.join(this.workspaceRoot, '.dlang', 'packages');
|
|
297
|
-
this.gitResolver = new GitUrlResolver(cacheDir);
|
|
216
|
+
this.gitResolver = new GitUrlResolver();
|
|
298
217
|
const loaded = await this.loadLockFileFromDisk();
|
|
299
218
|
this.applyLockFile(loaded);
|
|
300
219
|
|
|
301
|
-
if (!this.lockFile && this.options.autoResolve !== false
|
|
220
|
+
if (!this.lockFile && this.options.autoResolve !== false) {
|
|
302
221
|
await this.generateLockFile();
|
|
303
222
|
}
|
|
304
223
|
}
|
|
@@ -336,8 +255,8 @@ export class WorkspaceManager {
|
|
|
336
255
|
}
|
|
337
256
|
|
|
338
257
|
const lockFile = await resolver.resolveDependencies();
|
|
339
|
-
|
|
340
|
-
|
|
258
|
+
this.lockFile = lockFile;
|
|
259
|
+
this.gitResolver.setLockFile(lockFile);
|
|
341
260
|
|
|
342
261
|
// Write JSON lock file
|
|
343
262
|
await this.writeJsonLockFile(lockFile);
|
|
@@ -414,10 +333,6 @@ export class WorkspaceManager {
|
|
|
414
333
|
|
|
415
334
|
const content = await fs.readFile(manifestPath, 'utf-8');
|
|
416
335
|
const manifest = (YAML.parse(content) ?? {}) as ModelManifest;
|
|
417
|
-
|
|
418
|
-
// Validate manifest structure
|
|
419
|
-
this.validateManifest(manifest, manifestPath);
|
|
420
|
-
|
|
421
336
|
this.manifestCache = {
|
|
422
337
|
manifest,
|
|
423
338
|
path: manifestPath,
|
|
@@ -433,111 +348,6 @@ export class WorkspaceManager {
|
|
|
433
348
|
}
|
|
434
349
|
}
|
|
435
350
|
|
|
436
|
-
/**
|
|
437
|
-
* Validates manifest structure and dependency configurations.
|
|
438
|
-
* Throws detailed errors for invalid manifests.
|
|
439
|
-
*
|
|
440
|
-
* Supports both new format (owner/package: version) and extended format.
|
|
441
|
-
*/
|
|
442
|
-
private validateManifest(manifest: ModelManifest, manifestPath: string): void {
|
|
443
|
-
// Validate path aliases
|
|
444
|
-
if (manifest.paths) {
|
|
445
|
-
this.validatePathAliases(manifest.paths, manifestPath);
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
if (!manifest.dependencies) {
|
|
449
|
-
return; // No dependencies to validate
|
|
450
|
-
}
|
|
451
|
-
|
|
452
|
-
for (const [key, dep] of Object.entries(manifest.dependencies)) {
|
|
453
|
-
const normalized = this.normalizeDependency(key, dep);
|
|
454
|
-
|
|
455
|
-
// Validate mutually exclusive source and path
|
|
456
|
-
if (normalized.source && normalized.path) {
|
|
457
|
-
throw new Error(
|
|
458
|
-
`Invalid dependency '${key}' in ${manifestPath}:\n` +
|
|
459
|
-
`Cannot specify both 'source' and 'path'.\n` +
|
|
460
|
-
`Hint: Use 'source' for git dependencies or 'path' for local workspace dependencies.`
|
|
461
|
-
);
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
// For string format, source is always derived from key (valid)
|
|
465
|
-
// For extended format without source or path, error
|
|
466
|
-
if (typeof dep !== 'string' && !normalized.source && !normalized.path) {
|
|
467
|
-
throw new Error(
|
|
468
|
-
`Invalid dependency '${key}' in ${manifestPath}:\n` +
|
|
469
|
-
`Must specify either 'source' or 'path'.\n` +
|
|
470
|
-
`Hint: Add 'source: owner/repo' for git dependencies, or 'path: ./local/path' for local packages.`
|
|
471
|
-
);
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
// Validate path is relative and within workspace
|
|
475
|
-
if (normalized.path) {
|
|
476
|
-
this.validateLocalPath(normalized.path, key, manifestPath);
|
|
477
|
-
}
|
|
478
|
-
|
|
479
|
-
// Validate source has ref when specified
|
|
480
|
-
if (normalized.source && !normalized.ref) {
|
|
481
|
-
throw new Error(
|
|
482
|
-
`Invalid dependency '${key}' in ${manifestPath}:\n` +
|
|
483
|
-
`Git dependencies must specify a 'ref' (git reference).\n` +
|
|
484
|
-
`Hint: Add 'ref: v1.0.0' (tag), 'ref: main' (branch), or a commit SHA.`
|
|
485
|
-
);
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
/**
|
|
491
|
-
* Validates path aliases for security and correctness.
|
|
492
|
-
*/
|
|
493
|
-
private validatePathAliases(paths: PathAliases, manifestPath: string): void {
|
|
494
|
-
for (const [alias, targetPath] of Object.entries(paths)) {
|
|
495
|
-
// Validate alias starts with @
|
|
496
|
-
if (!alias.startsWith('@')) {
|
|
497
|
-
throw new Error(
|
|
498
|
-
`Invalid path alias '${alias}' in ${manifestPath}:\n` +
|
|
499
|
-
`Path aliases must start with '@'.\n` +
|
|
500
|
-
`Hint: Rename to '@${alias}' in your model.yaml paths section.`
|
|
501
|
-
);
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
// Validate target path doesn't escape workspace
|
|
505
|
-
this.validateLocalPath(targetPath, alias, manifestPath);
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
/**
|
|
510
|
-
* Validates local path dependencies for security.
|
|
511
|
-
* Ensures paths don't escape workspace boundary.
|
|
512
|
-
*/
|
|
513
|
-
private validateLocalPath(localPath: string, alias: string, manifestPath: string): void {
|
|
514
|
-
// Reject absolute paths
|
|
515
|
-
if (path.isAbsolute(localPath)) {
|
|
516
|
-
throw new Error(
|
|
517
|
-
`Invalid local path '${alias}' in ${manifestPath}:\n` +
|
|
518
|
-
`Cannot use absolute path '${localPath}'.\n` +
|
|
519
|
-
`Hint: Use relative paths (e.g., './lib', '../shared') for local dependencies.`
|
|
520
|
-
);
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
// Resolve path relative to manifest directory
|
|
524
|
-
const manifestDir = path.dirname(manifestPath);
|
|
525
|
-
const resolvedPath = path.resolve(manifestDir, localPath);
|
|
526
|
-
const workspaceRoot = this.workspaceRoot || manifestDir;
|
|
527
|
-
|
|
528
|
-
// Check if resolved path is within workspace
|
|
529
|
-
const relativePath = path.relative(workspaceRoot, resolvedPath);
|
|
530
|
-
if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
|
|
531
|
-
throw new Error(
|
|
532
|
-
`Invalid local path '${alias}' in ${manifestPath}:\n` +
|
|
533
|
-
`Path '${localPath}' resolves outside workspace boundary.\n` +
|
|
534
|
-
`Resolved: ${resolvedPath}\n` +
|
|
535
|
-
`Workspace: ${workspaceRoot}\n` +
|
|
536
|
-
`Hint: Local dependencies must be within the workspace. Consider moving the dependency or using a git-based source.`
|
|
537
|
-
);
|
|
538
|
-
}
|
|
539
|
-
}
|
|
540
|
-
|
|
541
351
|
private parseJsonLockFile(content: string): LockFile {
|
|
542
352
|
const parsed = JSON.parse(content) as Partial<LockFile> & {
|
|
543
353
|
dependencies?: Record<string, Partial<LockedDependency>>;
|
|
@@ -547,12 +357,11 @@ export class WorkspaceManager {
|
|
|
547
357
|
const dependencies: Record<string, LockedDependency> = {};
|
|
548
358
|
|
|
549
359
|
for (const [key, value] of Object.entries(parsed.dependencies ?? {})) {
|
|
550
|
-
if (!value || typeof value.
|
|
360
|
+
if (!value || typeof value.version !== 'string' || typeof value.resolved !== 'string' || typeof value.commit !== 'string') {
|
|
551
361
|
continue;
|
|
552
362
|
}
|
|
553
363
|
dependencies[key] = {
|
|
554
|
-
|
|
555
|
-
refType: value.refType ?? 'commit', // Default to commit for backwards compatibility
|
|
364
|
+
version: value.version,
|
|
556
365
|
resolved: value.resolved,
|
|
557
366
|
commit: value.commit,
|
|
558
367
|
integrity: value.integrity,
|