@atomic-ehr/fhir-canonical-manager 0.0.15 → 0.0.16
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/dist/cache.d.ts +2 -0
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +27 -2
- package/dist/cache.js.map +1 -1
- package/dist/local.d.ts +5 -0
- package/dist/local.d.ts.map +1 -0
- package/dist/local.js +148 -0
- package/dist/local.js.map +1 -0
- package/dist/manager/canonical.d.ts.map +1 -1
- package/dist/manager/canonical.js +205 -24
- package/dist/manager/canonical.js.map +1 -1
- package/dist/manager/package-spec.d.ts +5 -0
- package/dist/manager/package-spec.d.ts.map +1 -0
- package/dist/manager/package-spec.js +28 -0
- package/dist/manager/package-spec.js.map +1 -0
- package/dist/package.d.ts.map +1 -1
- package/dist/package.js +25 -5
- package/dist/package.js.map +1 -1
- package/dist/types/core.d.ts +12 -0
- package/dist/types/core.d.ts.map +1 -1
- package/dist/types/internal.d.ts +4 -2
- package/dist/types/internal.d.ts.map +1 -1
- package/package.json +2 -3
- package/src/cache.ts +0 -59
- package/src/cli/index.ts +0 -181
- package/src/cli/init.ts +0 -112
- package/src/cli/list.ts +0 -95
- package/src/cli/resolve.ts +0 -63
- package/src/cli/search.ts +0 -83
- package/src/cli/searchparam.ts +0 -163
- package/src/constants.ts +0 -6
- package/src/fs/index.ts +0 -5
- package/src/fs/utils.ts +0 -28
- package/src/index.ts +0 -18
- package/src/manager/canonical.ts +0 -359
- package/src/manager/index.ts +0 -6
- package/src/package.ts +0 -79
- package/src/reference.ts +0 -77
- package/src/resolver.ts +0 -38
- package/src/scanner/directory.ts +0 -36
- package/src/scanner/index.ts +0 -8
- package/src/scanner/package.ts +0 -35
- package/src/scanner/parser.ts +0 -40
- package/src/scanner/processor.ts +0 -65
- package/src/search/index.ts +0 -6
- package/src/search/smart.ts +0 -50
- package/src/search/terms.ts +0 -22
- package/src/types/core.ts +0 -131
- package/src/types/index.ts +0 -6
- package/src/types/internal.ts +0 -59
package/src/manager/canonical.ts
DELETED
|
@@ -1,359 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Main CanonicalManager implementation
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import * as afs from "node:fs/promises";
|
|
6
|
-
import * as Path from "node:path";
|
|
7
|
-
import { cacheRecordPaths, createCacheRecord, loadCacheRecordFromDisk, saveCacheRecordToDisk } from "../cache.js";
|
|
8
|
-
import { DEFAULT_REGISTRY } from "../constants.js";
|
|
9
|
-
import { ensureDir } from "../fs/index.js";
|
|
10
|
-
import { installPackages } from "../package.js";
|
|
11
|
-
import { resolveWithContext } from "../resolver.js";
|
|
12
|
-
import { scanDirectory } from "../scanner/index.js";
|
|
13
|
-
import { filterBySmartSearch } from "../search/index.js";
|
|
14
|
-
import type {
|
|
15
|
-
CanonicalManager,
|
|
16
|
-
Config,
|
|
17
|
-
IndexEntry,
|
|
18
|
-
PackageId,
|
|
19
|
-
PackageInfo,
|
|
20
|
-
Reference,
|
|
21
|
-
Resource,
|
|
22
|
-
SearchParameter,
|
|
23
|
-
SourceContext,
|
|
24
|
-
} from "../types/index.js";
|
|
25
|
-
|
|
26
|
-
export const createCanonicalManager = (config: Config): CanonicalManager => {
|
|
27
|
-
const { packages = [], workingDir } = config;
|
|
28
|
-
// Ensure registry URL ends with /
|
|
29
|
-
let registry = DEFAULT_REGISTRY;
|
|
30
|
-
if (config.registry) {
|
|
31
|
-
registry = config.registry.endsWith("/") ? config.registry : `${config.registry}/`;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const cache = createCacheRecord();
|
|
35
|
-
let initialized = false;
|
|
36
|
-
const searchParamsCache = new Map<string, SearchParameter[]>();
|
|
37
|
-
|
|
38
|
-
const ensureInitialized = (): void => {
|
|
39
|
-
if (!initialized) {
|
|
40
|
-
throw new Error("CanonicalManager not initialized. Call init() first.");
|
|
41
|
-
}
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
const packageRefToPackageMeta = async () => {
|
|
45
|
-
ensureInitialized();
|
|
46
|
-
const { npmRootPackageJsonFile } = cacheRecordPaths(workingDir, packages);
|
|
47
|
-
const rootPackageDeps =
|
|
48
|
-
(
|
|
49
|
-
JSON.parse(await afs.readFile(npmRootPackageJsonFile, "utf8")) as {
|
|
50
|
-
dependencies?: Record<string, string>;
|
|
51
|
-
}
|
|
52
|
-
).dependencies ?? {};
|
|
53
|
-
const res: Record<string, PackageId> = {};
|
|
54
|
-
for (const pkgRef of packages) {
|
|
55
|
-
if (pkgRef.startsWith("http://") || pkgRef.startsWith("https://")) {
|
|
56
|
-
for (const [depName, depVersion] of Object.entries(rootPackageDeps)) {
|
|
57
|
-
if (depVersion === pkgRef) {
|
|
58
|
-
const packageInfo = cache.packages[depName];
|
|
59
|
-
if (!packageInfo) throw new Error(`Package not found: ${depName}`);
|
|
60
|
-
res[pkgRef] = { name: packageInfo.id.name, version: packageInfo.id.version };
|
|
61
|
-
break;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
} else {
|
|
65
|
-
const [name, version] = pkgRef.split("@");
|
|
66
|
-
if (!name) throw new Error(`Invalid FHIR package meta: ${pkgRef}`);
|
|
67
|
-
res[pkgRef] = { name, version: version ?? "latest" };
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
return res;
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
const init = async (): Promise<Record<string, PackageId>> => {
|
|
74
|
-
if (initialized) return packageRefToPackageMeta();
|
|
75
|
-
|
|
76
|
-
await ensureDir(workingDir);
|
|
77
|
-
const { cacheKey, npmPackagePath } = cacheRecordPaths(workingDir, packages);
|
|
78
|
-
|
|
79
|
-
const cachedData = await loadCacheRecordFromDisk(workingDir, cacheKey);
|
|
80
|
-
const isCacheValid = cachedData && cachedData.packageLockHash === cacheKey;
|
|
81
|
-
if (isCacheValid) {
|
|
82
|
-
// Restore cache from disk
|
|
83
|
-
cache.entries = cachedData.entries;
|
|
84
|
-
cache.packages = cachedData.packages;
|
|
85
|
-
Object.entries(cachedData.references).forEach(([id, metadata]) => {
|
|
86
|
-
cache.referenceManager.set(id, metadata);
|
|
87
|
-
});
|
|
88
|
-
} else {
|
|
89
|
-
await installPackages(packages, npmPackagePath, registry);
|
|
90
|
-
await scanDirectory(cache, npmPackagePath);
|
|
91
|
-
await saveCacheRecordToDisk(cache, workingDir, cacheKey);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
initialized = true;
|
|
95
|
-
return packageRefToPackageMeta();
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
const destroy = async (): Promise<void> => {
|
|
99
|
-
cache.entries = {};
|
|
100
|
-
cache.packages = {};
|
|
101
|
-
cache.referenceManager.clear();
|
|
102
|
-
searchParamsCache.clear();
|
|
103
|
-
initialized = false;
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
const getPackages = async (): Promise<PackageId[]> => {
|
|
107
|
-
ensureInitialized();
|
|
108
|
-
return Object.values(cache.packages).map((p: PackageInfo) => p.id);
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
const addPackages = async (...newPackages: string[]): Promise<Record<string, PackageId>> => {
|
|
112
|
-
if (newPackages.length === 0) return packageRefToPackageMeta();
|
|
113
|
-
|
|
114
|
-
// Check if packages already exists in packages
|
|
115
|
-
const packagesToAdd = newPackages.filter((pkg) => !packages.includes(pkg));
|
|
116
|
-
if (packagesToAdd.length !== 0) {
|
|
117
|
-
packages.push(...packagesToAdd);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
if (!initialized) {
|
|
121
|
-
await init();
|
|
122
|
-
return packageRefToPackageMeta();
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
if (packagesToAdd.length > 0) {
|
|
126
|
-
// TODO: very expensive, we can just copy/paste old cache to avoid reinstalling packages
|
|
127
|
-
await destroy();
|
|
128
|
-
await init();
|
|
129
|
-
}
|
|
130
|
-
return packageRefToPackageMeta();
|
|
131
|
-
};
|
|
132
|
-
|
|
133
|
-
const resolveEntry = async (
|
|
134
|
-
canonicalUrl: string,
|
|
135
|
-
options?: {
|
|
136
|
-
package?: string;
|
|
137
|
-
version?: string;
|
|
138
|
-
sourceContext?: SourceContext;
|
|
139
|
-
},
|
|
140
|
-
): Promise<IndexEntry> => {
|
|
141
|
-
ensureInitialized();
|
|
142
|
-
|
|
143
|
-
if (options?.sourceContext) {
|
|
144
|
-
const contextResolved = await resolveWithContext(canonicalUrl, options.sourceContext, cache, resolveEntry);
|
|
145
|
-
if (contextResolved) {
|
|
146
|
-
return contextResolved;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const entries = cache.entries[canonicalUrl] || [];
|
|
151
|
-
|
|
152
|
-
if (entries.length === 0) {
|
|
153
|
-
throw new Error(`Cannot resolve canonical URL: ${canonicalUrl}`);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
let filtered = [...entries];
|
|
157
|
-
|
|
158
|
-
if (options?.package) {
|
|
159
|
-
filtered = filtered.filter((e) => e.package?.name === options.package);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
if (options?.version) {
|
|
163
|
-
filtered = filtered.filter((e) => e.version === options.version);
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
if (filtered.length === 0) {
|
|
167
|
-
throw new Error(`No matching resource found for ${canonicalUrl} with given options`);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const result = filtered[0];
|
|
171
|
-
if (!result) {
|
|
172
|
-
throw new Error(`No matching resource found for ${canonicalUrl}`);
|
|
173
|
-
}
|
|
174
|
-
return result;
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
const resolve = async (
|
|
178
|
-
canonicalUrl: string,
|
|
179
|
-
options?: {
|
|
180
|
-
package?: string;
|
|
181
|
-
version?: string;
|
|
182
|
-
sourceContext?: SourceContext;
|
|
183
|
-
},
|
|
184
|
-
): Promise<Resource> => {
|
|
185
|
-
const entry = await resolveEntry(canonicalUrl, options);
|
|
186
|
-
return read(entry);
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
const read = async (reference: Reference): Promise<Resource> => {
|
|
190
|
-
ensureInitialized();
|
|
191
|
-
|
|
192
|
-
const metadata = cache.referenceManager.get(reference.id);
|
|
193
|
-
if (!metadata) {
|
|
194
|
-
throw new Error(`Invalid reference ID: ${reference.id}`);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
try {
|
|
198
|
-
const content = await afs.readFile(metadata.filePath, "utf-8");
|
|
199
|
-
const resource = JSON.parse(content);
|
|
200
|
-
|
|
201
|
-
return {
|
|
202
|
-
...resource,
|
|
203
|
-
id: reference.id,
|
|
204
|
-
resourceType: reference.resourceType,
|
|
205
|
-
};
|
|
206
|
-
} catch (err) {
|
|
207
|
-
throw new Error(`Failed to read resource: ${err}`);
|
|
208
|
-
}
|
|
209
|
-
};
|
|
210
|
-
|
|
211
|
-
const searchEntries = async (params: {
|
|
212
|
-
kind?: string;
|
|
213
|
-
url?: string;
|
|
214
|
-
type?: string;
|
|
215
|
-
version?: string;
|
|
216
|
-
package?: PackageId;
|
|
217
|
-
}): Promise<IndexEntry[]> => {
|
|
218
|
-
ensureInitialized();
|
|
219
|
-
|
|
220
|
-
let results: IndexEntry[] = [];
|
|
221
|
-
|
|
222
|
-
if (params.url) {
|
|
223
|
-
results = cache.entries[params.url] || [];
|
|
224
|
-
} else {
|
|
225
|
-
for (const entries of Object.values(cache.entries)) {
|
|
226
|
-
results.push(...entries);
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
if (params.kind !== undefined) {
|
|
231
|
-
results = results.filter((e) => e.kind === params.kind);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
if (params.type !== undefined) {
|
|
235
|
-
results = results.filter((e) => e.type === params.type);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
if (params.version !== undefined) {
|
|
239
|
-
results = results.filter((e) => e.version === params.version);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
if (params.package) {
|
|
243
|
-
const pkg = params.package;
|
|
244
|
-
results = results.filter((e) => e.package?.name === pkg.name && e.package?.version === pkg.version);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
return results;
|
|
248
|
-
};
|
|
249
|
-
|
|
250
|
-
const search = async (params: {
|
|
251
|
-
kind?: string;
|
|
252
|
-
url?: string;
|
|
253
|
-
type?: string;
|
|
254
|
-
version?: string;
|
|
255
|
-
package?: PackageId;
|
|
256
|
-
}): Promise<Resource[]> => {
|
|
257
|
-
const entries = await searchEntries(params);
|
|
258
|
-
const resources = await Promise.all(entries.map((entry) => read(entry)));
|
|
259
|
-
return resources;
|
|
260
|
-
};
|
|
261
|
-
|
|
262
|
-
const smartSearch = async (
|
|
263
|
-
searchTerms: string[],
|
|
264
|
-
filters?: {
|
|
265
|
-
resourceType?: string;
|
|
266
|
-
type?: string;
|
|
267
|
-
kind?: string;
|
|
268
|
-
package?: PackageId;
|
|
269
|
-
},
|
|
270
|
-
): Promise<IndexEntry[]> => {
|
|
271
|
-
ensureInitialized();
|
|
272
|
-
|
|
273
|
-
// Start with base search using filters
|
|
274
|
-
let results = await searchEntries({
|
|
275
|
-
kind: filters?.kind,
|
|
276
|
-
package: filters?.package,
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
// Apply resourceType filter
|
|
280
|
-
if (filters?.resourceType) {
|
|
281
|
-
results = results.filter((entry) => entry.resourceType === filters.resourceType);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
// Apply type filter
|
|
285
|
-
if (filters?.type) {
|
|
286
|
-
results = results.filter((entry) => entry.type === filters.type);
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// Apply smart search filtering
|
|
290
|
-
return filterBySmartSearch(results, searchTerms);
|
|
291
|
-
};
|
|
292
|
-
|
|
293
|
-
const getSearchParametersForResource = async (resourceType: string): Promise<SearchParameter[]> => {
|
|
294
|
-
ensureInitialized();
|
|
295
|
-
|
|
296
|
-
// Check cache first
|
|
297
|
-
if (searchParamsCache.has(resourceType)) {
|
|
298
|
-
const cached = searchParamsCache.get(resourceType);
|
|
299
|
-
if (cached) {
|
|
300
|
-
return cached;
|
|
301
|
-
}
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
// Query all SearchParameter resources
|
|
305
|
-
// Note: We search by resourceType, not type, because for SearchParameter resources,
|
|
306
|
-
// 'type' refers to the search parameter type (token, string, etc.)
|
|
307
|
-
const allEntries = await searchEntries({});
|
|
308
|
-
const searchParamEntries = allEntries.filter((entry) => entry.resourceType === "SearchParameter");
|
|
309
|
-
|
|
310
|
-
const results: SearchParameter[] = [];
|
|
311
|
-
|
|
312
|
-
for (const entry of searchParamEntries) {
|
|
313
|
-
const resource = await read(entry);
|
|
314
|
-
|
|
315
|
-
// Check if this parameter applies to the requested resource
|
|
316
|
-
const bases = resource.base || [];
|
|
317
|
-
if (Array.isArray(bases) && bases.includes(resourceType)) {
|
|
318
|
-
// Return the full original resource - it already contains all fields
|
|
319
|
-
// Cast through unknown to satisfy TypeScript
|
|
320
|
-
results.push(resource as unknown as SearchParameter);
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// Sort by code for consistent output
|
|
325
|
-
results.sort((a, b) => {
|
|
326
|
-
const codeA = a.code || "";
|
|
327
|
-
const codeB = b.code || "";
|
|
328
|
-
return codeA.localeCompare(codeB);
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
// Cache the results
|
|
332
|
-
searchParamsCache.set(resourceType, results);
|
|
333
|
-
|
|
334
|
-
return results;
|
|
335
|
-
};
|
|
336
|
-
|
|
337
|
-
const packageJson = async (packageName: string) => {
|
|
338
|
-
ensureInitialized();
|
|
339
|
-
const fn = cache.packages[packageName]?.path;
|
|
340
|
-
if (!fn) throw new Error(`Package ${packageName} not found`);
|
|
341
|
-
const packageJSON = JSON.parse(await afs.readFile(Path.join(fn, "package.json"), "utf8"));
|
|
342
|
-
return packageJSON;
|
|
343
|
-
};
|
|
344
|
-
|
|
345
|
-
return {
|
|
346
|
-
init,
|
|
347
|
-
destroy,
|
|
348
|
-
packages: getPackages,
|
|
349
|
-
addPackages,
|
|
350
|
-
resolveEntry,
|
|
351
|
-
resolve,
|
|
352
|
-
read,
|
|
353
|
-
searchEntries,
|
|
354
|
-
search,
|
|
355
|
-
smartSearch,
|
|
356
|
-
getSearchParametersForResource,
|
|
357
|
-
packageJson,
|
|
358
|
-
};
|
|
359
|
-
};
|
package/src/manager/index.ts
DELETED
package/src/package.ts
DELETED
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Package management functionality
|
|
3
|
-
* Merged from package/detector.ts, package/installer.ts, and package/index.ts
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { exec } from "node:child_process";
|
|
7
|
-
import * as afs from "node:fs/promises";
|
|
8
|
-
import * as Path from "node:path";
|
|
9
|
-
import { promisify } from "node:util";
|
|
10
|
-
import { ensureDir, fileExists } from "./fs/index.js";
|
|
11
|
-
|
|
12
|
-
const execAsync = promisify(exec);
|
|
13
|
-
|
|
14
|
-
export type PackageManager = "bun" | "npm";
|
|
15
|
-
|
|
16
|
-
export const detectPackageManager = async (): Promise<PackageManager | undefined> => {
|
|
17
|
-
try {
|
|
18
|
-
await execAsync("bun --version");
|
|
19
|
-
return "bun";
|
|
20
|
-
} catch {
|
|
21
|
-
try {
|
|
22
|
-
await execAsync("npm --version");
|
|
23
|
-
return "npm";
|
|
24
|
-
} catch {
|
|
25
|
-
return;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const ensurePackageJson = async (pwd: string) => {
|
|
31
|
-
const packageJsonPath = Path.join(pwd, "package.json");
|
|
32
|
-
if (!(await fileExists(packageJsonPath))) {
|
|
33
|
-
const minimalPackageJson = {
|
|
34
|
-
name: "fhir-canonical-manager-workspace",
|
|
35
|
-
version: "1.0.0",
|
|
36
|
-
private: true,
|
|
37
|
-
dependencies: {},
|
|
38
|
-
};
|
|
39
|
-
await afs.writeFile(packageJsonPath, JSON.stringify(minimalPackageJson, null, 2));
|
|
40
|
-
}
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
export const installPackages = async (packages: string[], pwd: string, registry?: string): Promise<void> => {
|
|
44
|
-
await ensureDir(pwd);
|
|
45
|
-
ensurePackageJson(pwd);
|
|
46
|
-
|
|
47
|
-
const packageManager = await detectPackageManager();
|
|
48
|
-
if (!packageManager) throw new Error("No package manager found. Please install bun or npm.");
|
|
49
|
-
|
|
50
|
-
for (const pkg of packages) {
|
|
51
|
-
try {
|
|
52
|
-
if (packageManager === "bun") {
|
|
53
|
-
// Use bun with auth bypass trick for FHIR registry
|
|
54
|
-
const env = {
|
|
55
|
-
...process.env,
|
|
56
|
-
HOME: pwd, // Prevent reading user's .npmrc
|
|
57
|
-
NPM_CONFIG_USERCONFIG: "/dev/null", // Extra safety
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
const cmd = registry
|
|
61
|
-
? `bun add ${pkg} --cwd='${pwd}' --registry='${registry}'`
|
|
62
|
-
: `bun add --cwd='${pwd}' ${pkg}`;
|
|
63
|
-
await execAsync(cmd, {
|
|
64
|
-
env,
|
|
65
|
-
maxBuffer: 10 * 1024 * 1024, // 10MB buffer
|
|
66
|
-
});
|
|
67
|
-
} else {
|
|
68
|
-
// Use npm (handles auth correctly)
|
|
69
|
-
const cmd = registry ? `cd ${pwd} && npm add ${pkg} --registry=${registry}` : `npm add ${pkg}`;
|
|
70
|
-
await execAsync(cmd, {
|
|
71
|
-
maxBuffer: 10 * 1024 * 1024, // 10MB buffer
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
} catch (err) {
|
|
75
|
-
console.error(`Failed to install package ${pkg}:`, err);
|
|
76
|
-
throw err;
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
};
|
package/src/reference.ts
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Reference management module
|
|
3
|
-
* Merged from reference/index.ts, reference/manager.ts, and reference/store.ts
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { createHash } from "node:crypto";
|
|
7
|
-
import type { Reference, ReferenceMetadata, ReferenceStore } from "./types/index.js";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Generate a unique reference ID from metadata
|
|
11
|
-
*/
|
|
12
|
-
export const generateReferenceId = (metadata: {
|
|
13
|
-
packageName: string;
|
|
14
|
-
packageVersion: string;
|
|
15
|
-
filePath: string;
|
|
16
|
-
}): string => {
|
|
17
|
-
const input = `${metadata.packageName}@${metadata.packageVersion}:${metadata.filePath}`;
|
|
18
|
-
return createHash("sha256").update(input).digest("base64url");
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Reference manager interface
|
|
23
|
-
*/
|
|
24
|
-
export interface ReferenceManager extends ReferenceStore {
|
|
25
|
-
generateId: typeof generateReferenceId;
|
|
26
|
-
getIdsByUrl: (url: string) => string[];
|
|
27
|
-
createReference: (id: string, metadata: ReferenceMetadata) => Reference;
|
|
28
|
-
getAllReferences: () => Record<string, ReferenceMetadata>;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Create a reference manager instance
|
|
33
|
-
*/
|
|
34
|
-
export const createReferenceManager = (): ReferenceManager => {
|
|
35
|
-
const references: Record<string, ReferenceMetadata> = {};
|
|
36
|
-
const urlToIds: Record<string, string[]> = {};
|
|
37
|
-
|
|
38
|
-
const set = (id: string, metadata: ReferenceMetadata): void => {
|
|
39
|
-
references[id] = metadata;
|
|
40
|
-
if (metadata.url) {
|
|
41
|
-
if (!urlToIds[metadata.url]) {
|
|
42
|
-
urlToIds[metadata.url] = [];
|
|
43
|
-
}
|
|
44
|
-
const ids = urlToIds[metadata.url];
|
|
45
|
-
if (ids && !ids.includes(id)) {
|
|
46
|
-
ids.push(id);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
const clear = (): void => {
|
|
52
|
-
Object.keys(references).forEach((key) => {
|
|
53
|
-
delete references[key];
|
|
54
|
-
});
|
|
55
|
-
Object.keys(urlToIds).forEach((key) => {
|
|
56
|
-
delete urlToIds[key];
|
|
57
|
-
});
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
return {
|
|
61
|
-
generateId: generateReferenceId,
|
|
62
|
-
get: (id: string) => references[id],
|
|
63
|
-
set,
|
|
64
|
-
has: (id: string) => id in references,
|
|
65
|
-
clear,
|
|
66
|
-
size: () => Object.keys(references).length,
|
|
67
|
-
getIdsByUrl: (url: string) => urlToIds[url] || [],
|
|
68
|
-
createReference: (id: string, metadata: ReferenceMetadata): Reference => ({
|
|
69
|
-
id,
|
|
70
|
-
resourceType: metadata.resourceType,
|
|
71
|
-
}),
|
|
72
|
-
getAllReferences: () => references,
|
|
73
|
-
};
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
// For backward compatibility - function alias (will be deprecated)
|
|
77
|
-
export { createReferenceManager as ReferenceManagerFactory };
|
package/src/resolver.ts
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Resolver module - Context-aware resolution functionality
|
|
3
|
-
* Merged from resolver/index.ts and resolver/context.ts
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { ExtendedCache } from "./cache.js";
|
|
7
|
-
import type { IndexEntry, SourceContext } from "./types/index.js";
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* Resolve a canonical URL with context awareness
|
|
11
|
-
*
|
|
12
|
-
* Attempts to resolve a URL within a specific package context first,
|
|
13
|
-
* then falls back to global resolution if the context-specific resolution fails.
|
|
14
|
-
*
|
|
15
|
-
* @param url - The canonical URL to resolve
|
|
16
|
-
* @param context - The source context containing package information
|
|
17
|
-
* @param _cache - The extended cache instance (currently unused but kept for future use)
|
|
18
|
-
* @param resolveEntry - Function to resolve an entry by URL with optional package/version
|
|
19
|
-
* @returns The resolved index entry or null if resolution fails
|
|
20
|
-
*/
|
|
21
|
-
export const resolveWithContext = async (
|
|
22
|
-
url: string,
|
|
23
|
-
context: SourceContext,
|
|
24
|
-
_cache: ExtendedCache,
|
|
25
|
-
resolveEntry: (url: string, options?: any) => Promise<IndexEntry>,
|
|
26
|
-
): Promise<IndexEntry | null> => {
|
|
27
|
-
if (context.package) {
|
|
28
|
-
try {
|
|
29
|
-
return await resolveEntry(url, {
|
|
30
|
-
package: context.package.name,
|
|
31
|
-
version: context.package.version,
|
|
32
|
-
});
|
|
33
|
-
} catch {
|
|
34
|
-
// Fall through to global resolution
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
return null;
|
|
38
|
-
};
|
package/src/scanner/directory.ts
DELETED
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Directory scanning functionality
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import * as fs from "node:fs/promises";
|
|
6
|
-
import * as path from "node:path";
|
|
7
|
-
import type { ExtendedCache } from "../cache.js";
|
|
8
|
-
import { isFhirPackage } from "../fs/index.js";
|
|
9
|
-
import { scanPackage } from "./package.js";
|
|
10
|
-
|
|
11
|
-
export const scanDirectory = async (cache: ExtendedCache, pwd: string): Promise<void> => {
|
|
12
|
-
const nodeModulesPath = path.join(pwd, "node_modules");
|
|
13
|
-
const entries = await fs.readdir(nodeModulesPath, { withFileTypes: true });
|
|
14
|
-
for (const entry of entries) {
|
|
15
|
-
if (!entry.isDirectory()) continue;
|
|
16
|
-
|
|
17
|
-
const fullPath = path.join(entry.parentPath, entry.name);
|
|
18
|
-
|
|
19
|
-
if (entry.name.startsWith("@")) {
|
|
20
|
-
const scopedEntries = await fs.readdir(fullPath, {
|
|
21
|
-
withFileTypes: true,
|
|
22
|
-
});
|
|
23
|
-
for (const scopedEntry of scopedEntries) {
|
|
24
|
-
if (!scopedEntry.isDirectory()) continue;
|
|
25
|
-
|
|
26
|
-
const scopedPath = path.join(fullPath, scopedEntry.name);
|
|
27
|
-
|
|
28
|
-
if (await isFhirPackage(scopedPath)) {
|
|
29
|
-
await scanPackage(scopedPath, cache);
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
} else if (await isFhirPackage(fullPath)) {
|
|
33
|
-
await scanPackage(fullPath, cache);
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
};
|
package/src/scanner/index.ts
DELETED
package/src/scanner/package.ts
DELETED
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Package scanning functionality
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import * as fs from "node:fs/promises";
|
|
6
|
-
import * as path from "node:path";
|
|
7
|
-
import type { ExtendedCache } from "../cache.js";
|
|
8
|
-
import { fileExists } from "../fs/index.js";
|
|
9
|
-
import type { PackageInfo, PackageJson } from "../types/index.js";
|
|
10
|
-
import { processIndex } from "./processor.js";
|
|
11
|
-
|
|
12
|
-
export const scanPackage = async (packagePath: string, cache: ExtendedCache): Promise<void> => {
|
|
13
|
-
try {
|
|
14
|
-
const packageJsonPath = path.join(packagePath, "package.json");
|
|
15
|
-
const packageJsonContent = await fs.readFile(packageJsonPath, "utf-8");
|
|
16
|
-
const packageJson: PackageJson = JSON.parse(packageJsonContent);
|
|
17
|
-
|
|
18
|
-
const packageInfo: PackageInfo = {
|
|
19
|
-
id: { name: packageJson.name, version: packageJson.version },
|
|
20
|
-
path: packagePath,
|
|
21
|
-
canonical: packageJson.canonical,
|
|
22
|
-
fhirVersions: packageJson.fhirVersions,
|
|
23
|
-
};
|
|
24
|
-
cache.packages[packageJson.name] = packageInfo;
|
|
25
|
-
|
|
26
|
-
await processIndex(packagePath, packageJson, cache);
|
|
27
|
-
|
|
28
|
-
const examplesPath = path.join(packagePath, "examples");
|
|
29
|
-
if (await fileExists(path.join(examplesPath, ".index.json"))) {
|
|
30
|
-
await processIndex(examplesPath, packageJson, cache);
|
|
31
|
-
}
|
|
32
|
-
} catch {
|
|
33
|
-
// Silently ignore package scan errors
|
|
34
|
-
}
|
|
35
|
-
};
|
package/src/scanner/parser.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Index file parsing and validation
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import type { IndexFile } from "../types/index.js";
|
|
6
|
-
|
|
7
|
-
export const isValidFileEntry = (entry: any): boolean => {
|
|
8
|
-
if (!entry || typeof entry !== "object") return false;
|
|
9
|
-
if (!entry.filename || typeof entry.filename !== "string") return false;
|
|
10
|
-
if (!entry.resourceType || typeof entry.resourceType !== "string") return false;
|
|
11
|
-
if (!entry.id || typeof entry.id !== "string") return false;
|
|
12
|
-
|
|
13
|
-
const optionalStringFields = ["url", "version", "kind", "type"];
|
|
14
|
-
for (const field of optionalStringFields) {
|
|
15
|
-
if (entry[field] !== undefined && typeof entry[field] !== "string") {
|
|
16
|
-
return false;
|
|
17
|
-
}
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
return true;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
export const isValidIndexFile = (data: any): boolean => {
|
|
24
|
-
if (!data || typeof data !== "object") return false;
|
|
25
|
-
if (!data["index-version"] || typeof data["index-version"] !== "number") return false;
|
|
26
|
-
if (!Array.isArray(data.files)) return false;
|
|
27
|
-
return data.files.every((file: any) => isValidFileEntry(file));
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
export const parseIndex = (content: string, _filePath: string): IndexFile | null => {
|
|
31
|
-
try {
|
|
32
|
-
const data = JSON.parse(content);
|
|
33
|
-
if (!isValidIndexFile(data)) {
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
return data as IndexFile;
|
|
37
|
-
} catch {
|
|
38
|
-
return null;
|
|
39
|
-
}
|
|
40
|
-
};
|