@atomic-ehr/fhir-canonical-manager 0.0.6 → 0.0.8
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/README.md +30 -30
- package/dist/cli/index.d.ts +2 -2
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +45 -35
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +25 -16
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/list.d.ts.map +1 -1
- package/dist/cli/list.js +6 -0
- package/dist/cli/list.js.map +1 -1
- package/dist/cli/resolve.d.ts.map +1 -1
- package/dist/cli/resolve.js +9 -0
- package/dist/cli/resolve.js.map +1 -1
- package/dist/cli/search.d.ts.map +1 -1
- package/dist/cli/search.js +16 -22
- package/dist/cli/search.js.map +1 -1
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +239 -64
- package/dist/index.js.map +1 -1
- package/package.json +4 -3
- package/src/cli/index.ts +53 -39
- package/src/cli/init.ts +36 -23
- package/src/cli/list.ts +6 -0
- package/src/cli/resolve.ts +9 -0
- package/src/cli/search.ts +20 -36
- package/src/index.ts +443 -146
- package/dist/compat.d.ts +0 -17
- package/dist/compat.d.ts.map +0 -1
- package/dist/compat.js +0 -56
- package/dist/compat.js.map +0 -1
- package/src/compat.ts +0 -98
package/src/index.ts
CHANGED
|
@@ -3,10 +3,148 @@
|
|
|
3
3
|
* A package manager for FHIR resources with canonical URL resolution
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
import * as path from
|
|
7
|
-
import * as fs from
|
|
8
|
-
import { createHash } from
|
|
9
|
-
import {
|
|
6
|
+
import * as path from "path";
|
|
7
|
+
import * as fs from "fs/promises";
|
|
8
|
+
import { createHash } from "crypto";
|
|
9
|
+
import { exec } from "child_process";
|
|
10
|
+
import { promisify } from "util";
|
|
11
|
+
|
|
12
|
+
// Shell command utilities
|
|
13
|
+
const execAsync = promisify(exec);
|
|
14
|
+
|
|
15
|
+
// Shell error type
|
|
16
|
+
class ShellError extends Error {
|
|
17
|
+
exitCode: number;
|
|
18
|
+
stdout: string;
|
|
19
|
+
stderr: string;
|
|
20
|
+
|
|
21
|
+
constructor(
|
|
22
|
+
message: string,
|
|
23
|
+
exitCode: number,
|
|
24
|
+
stdout: string,
|
|
25
|
+
stderr: string,
|
|
26
|
+
) {
|
|
27
|
+
super(message);
|
|
28
|
+
this.name = "ShellError";
|
|
29
|
+
this.exitCode = exitCode;
|
|
30
|
+
this.stdout = stdout;
|
|
31
|
+
this.stderr = stderr;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface ShellResult {
|
|
36
|
+
stdout: string;
|
|
37
|
+
stderr: string;
|
|
38
|
+
exitCode: number;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface ShellPromise extends Promise<ShellResult> {
|
|
42
|
+
quiet(): Promise<ShellResult>;
|
|
43
|
+
env(variables: Record<string, string>): ShellPromise;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Shell command execution using standard Node.js child_process
|
|
48
|
+
*/
|
|
49
|
+
function $(strings: TemplateStringsArray, ...values: any[]): ShellPromise {
|
|
50
|
+
const command = strings.reduce((acc, str, i) => {
|
|
51
|
+
return acc + str + (values[i] || "");
|
|
52
|
+
}, "");
|
|
53
|
+
|
|
54
|
+
let envVars: Record<string, string> = {};
|
|
55
|
+
|
|
56
|
+
// Always use Node.js child_process for consistent behavior
|
|
57
|
+
const execute = async (options: { quiet?: boolean } = {}) => {
|
|
58
|
+
try {
|
|
59
|
+
const execOptions: any = {
|
|
60
|
+
shell: true,
|
|
61
|
+
maxBuffer: 10 * 1024 * 1024, // 10MB buffer
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Apply environment variables if set
|
|
65
|
+
if (Object.keys(envVars).length > 0) {
|
|
66
|
+
execOptions.env = { ...process.env, ...envVars };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const { stdout, stderr } = await execAsync(command, execOptions);
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
stdout: stdout?.toString() || "",
|
|
73
|
+
stderr: stderr?.toString() || "",
|
|
74
|
+
exitCode: 0,
|
|
75
|
+
};
|
|
76
|
+
} catch (error: any) {
|
|
77
|
+
// Extract error details
|
|
78
|
+
const code = error.code || 1;
|
|
79
|
+
const stdout = error.stdout?.toString() || "";
|
|
80
|
+
const stderr = error.stderr?.toString() || error.message || "";
|
|
81
|
+
|
|
82
|
+
throw new ShellError(`Command failed: ${command}`, code, stdout, stderr);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Create a lazy promise that only executes when awaited
|
|
87
|
+
let executed = false;
|
|
88
|
+
let resultPromise: Promise<ShellResult> | null = null;
|
|
89
|
+
|
|
90
|
+
const lazyPromise = {
|
|
91
|
+
then(onFulfilled?: any, onRejected?: any) {
|
|
92
|
+
if (!executed) {
|
|
93
|
+
executed = true;
|
|
94
|
+
resultPromise = execute();
|
|
95
|
+
}
|
|
96
|
+
return resultPromise!.then(onFulfilled, onRejected);
|
|
97
|
+
},
|
|
98
|
+
catch(onRejected?: any) {
|
|
99
|
+
return this.then(undefined, onRejected);
|
|
100
|
+
},
|
|
101
|
+
finally(onFinally?: any) {
|
|
102
|
+
return this.then(
|
|
103
|
+
(value: any) => {
|
|
104
|
+
onFinally?.();
|
|
105
|
+
return value;
|
|
106
|
+
},
|
|
107
|
+
(reason: any) => {
|
|
108
|
+
onFinally?.();
|
|
109
|
+
throw reason;
|
|
110
|
+
},
|
|
111
|
+
);
|
|
112
|
+
},
|
|
113
|
+
quiet() {
|
|
114
|
+
return execute({ quiet: true });
|
|
115
|
+
},
|
|
116
|
+
env(vars: Record<string, string>) {
|
|
117
|
+
envVars = vars;
|
|
118
|
+
return this;
|
|
119
|
+
},
|
|
120
|
+
// Add Symbol.toStringTag to satisfy Promise interface
|
|
121
|
+
[Symbol.toStringTag]: "ShellPromise" as const,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
return lazyPromise as any as ShellPromise;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Detect available package manager
|
|
129
|
+
*/
|
|
130
|
+
async function detectPackageManager(): Promise<"bun" | "npm" | null> {
|
|
131
|
+
try {
|
|
132
|
+
// Check for bun first
|
|
133
|
+
await $`bun --version`.quiet();
|
|
134
|
+
return "bun";
|
|
135
|
+
} catch {
|
|
136
|
+
try {
|
|
137
|
+
// Fall back to npm
|
|
138
|
+
await $`npm --version`.quiet();
|
|
139
|
+
return "npm";
|
|
140
|
+
} catch {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Export shell utilities for testing
|
|
147
|
+
export { ShellError, detectPackageManager };
|
|
10
148
|
|
|
11
149
|
// Types
|
|
12
150
|
export interface Reference {
|
|
@@ -51,36 +189,51 @@ export interface CanonicalManager {
|
|
|
51
189
|
init(): Promise<void>;
|
|
52
190
|
destroy(): Promise<void>;
|
|
53
191
|
packages(): Promise<PackageId[]>;
|
|
54
|
-
resolveEntry(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
192
|
+
resolveEntry(
|
|
193
|
+
canonicalUrl: string,
|
|
194
|
+
options?: {
|
|
195
|
+
package?: string;
|
|
196
|
+
version?: string;
|
|
197
|
+
sourceContext?: SourceContext;
|
|
198
|
+
},
|
|
199
|
+
): Promise<IndexEntry>;
|
|
200
|
+
resolve(
|
|
201
|
+
canonicalUrl: string,
|
|
202
|
+
options?: {
|
|
203
|
+
package?: string;
|
|
204
|
+
version?: string;
|
|
205
|
+
sourceContext?: SourceContext;
|
|
206
|
+
},
|
|
207
|
+
): Promise<Resource>;
|
|
64
208
|
read(reference: Reference): Promise<Resource>;
|
|
65
209
|
searchEntries(params: {
|
|
66
|
-
kind?: string
|
|
67
|
-
url?: string
|
|
68
|
-
type?: string
|
|
69
|
-
version?: string
|
|
70
|
-
package?: PackageId
|
|
210
|
+
kind?: string;
|
|
211
|
+
url?: string;
|
|
212
|
+
type?: string;
|
|
213
|
+
version?: string;
|
|
214
|
+
package?: PackageId;
|
|
71
215
|
}): Promise<IndexEntry[]>;
|
|
72
216
|
search(params: {
|
|
73
|
-
kind?: string
|
|
74
|
-
url?: string
|
|
75
|
-
type?: string
|
|
76
|
-
version?: string
|
|
77
|
-
package?: PackageId
|
|
217
|
+
kind?: string;
|
|
218
|
+
url?: string;
|
|
219
|
+
type?: string;
|
|
220
|
+
version?: string;
|
|
221
|
+
package?: PackageId;
|
|
78
222
|
}): Promise<Resource[]>;
|
|
223
|
+
smartSearch(
|
|
224
|
+
searchTerms: string[],
|
|
225
|
+
filters?: {
|
|
226
|
+
resourceType?: string;
|
|
227
|
+
type?: string;
|
|
228
|
+
kind?: string;
|
|
229
|
+
package?: PackageId;
|
|
230
|
+
}
|
|
231
|
+
): Promise<IndexEntry[]>;
|
|
79
232
|
}
|
|
80
233
|
|
|
81
234
|
// Internal types
|
|
82
235
|
interface IndexFile {
|
|
83
|
-
|
|
236
|
+
"index-version": number;
|
|
84
237
|
files: IndexFileEntry[];
|
|
85
238
|
}
|
|
86
239
|
|
|
@@ -149,7 +302,7 @@ const generateReferenceId = (metadata: {
|
|
|
149
302
|
filePath: string;
|
|
150
303
|
}): string => {
|
|
151
304
|
const input = `${metadata.packageName}@${metadata.packageVersion}:${metadata.filePath}`;
|
|
152
|
-
return createHash(
|
|
305
|
+
return createHash("sha256").update(input).digest("base64url");
|
|
153
306
|
};
|
|
154
307
|
|
|
155
308
|
export const ReferenceManager = (): ReferenceStore & {
|
|
@@ -175,8 +328,8 @@ export const ReferenceManager = (): ReferenceStore & {
|
|
|
175
328
|
};
|
|
176
329
|
|
|
177
330
|
const clear = (): void => {
|
|
178
|
-
Object.keys(references).forEach(key => delete references[key]);
|
|
179
|
-
Object.keys(urlToIds).forEach(key => delete urlToIds[key]);
|
|
331
|
+
Object.keys(references).forEach((key) => delete references[key]);
|
|
332
|
+
Object.keys(urlToIds).forEach((key) => delete urlToIds[key]);
|
|
180
333
|
};
|
|
181
334
|
|
|
182
335
|
return {
|
|
@@ -189,32 +342,34 @@ export const ReferenceManager = (): ReferenceStore & {
|
|
|
189
342
|
getIdsByUrl: (url: string) => urlToIds[url] || [],
|
|
190
343
|
createReference: (id: string, metadata: ReferenceMetadata): Reference => ({
|
|
191
344
|
id,
|
|
192
|
-
resourceType: metadata.resourceType
|
|
345
|
+
resourceType: metadata.resourceType,
|
|
193
346
|
}),
|
|
194
|
-
getAllReferences: () => references
|
|
347
|
+
getAllReferences: () => references,
|
|
195
348
|
};
|
|
196
349
|
};
|
|
197
350
|
|
|
198
351
|
// Parser functions
|
|
199
352
|
const isValidFileEntry = (entry: any): boolean => {
|
|
200
|
-
if (!entry || typeof entry !==
|
|
201
|
-
if (!entry.filename || typeof entry.filename !==
|
|
202
|
-
if (!entry.resourceType || typeof entry.resourceType !==
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
353
|
+
if (!entry || typeof entry !== "object") return false;
|
|
354
|
+
if (!entry.filename || typeof entry.filename !== "string") return false;
|
|
355
|
+
if (!entry.resourceType || typeof entry.resourceType !== "string")
|
|
356
|
+
return false;
|
|
357
|
+
if (!entry.id || typeof entry.id !== "string") return false;
|
|
358
|
+
|
|
359
|
+
const optionalStringFields = ["url", "version", "kind", "type"];
|
|
206
360
|
for (const field of optionalStringFields) {
|
|
207
|
-
if (entry[field] !== undefined && typeof entry[field] !==
|
|
361
|
+
if (entry[field] !== undefined && typeof entry[field] !== "string") {
|
|
208
362
|
return false;
|
|
209
363
|
}
|
|
210
364
|
}
|
|
211
|
-
|
|
365
|
+
|
|
212
366
|
return true;
|
|
213
367
|
};
|
|
214
368
|
|
|
215
369
|
const isValidIndexFile = (data: any): boolean => {
|
|
216
|
-
if (!data || typeof data !==
|
|
217
|
-
if (!data[
|
|
370
|
+
if (!data || typeof data !== "object") return false;
|
|
371
|
+
if (!data["index-version"] || typeof data["index-version"] !== "number")
|
|
372
|
+
return false;
|
|
218
373
|
if (!Array.isArray(data.files)) return false;
|
|
219
374
|
return data.files.every((file: any) => isValidFileEntry(file));
|
|
220
375
|
};
|
|
@@ -250,7 +405,7 @@ const ensureDir = async (dirPath: string): Promise<void> => {
|
|
|
250
405
|
};
|
|
251
406
|
|
|
252
407
|
const isFhirPackage = async (dirPath: string): Promise<boolean> => {
|
|
253
|
-
const indexPath = path.join(dirPath,
|
|
408
|
+
const indexPath = path.join(dirPath, ".index.json");
|
|
254
409
|
return fileExists(indexPath);
|
|
255
410
|
};
|
|
256
411
|
|
|
@@ -263,40 +418,56 @@ const createCache = (): IndexCache & {
|
|
|
263
418
|
entries: {},
|
|
264
419
|
packages: {},
|
|
265
420
|
references: {},
|
|
266
|
-
referenceManager
|
|
421
|
+
referenceManager,
|
|
267
422
|
};
|
|
268
423
|
};
|
|
269
424
|
|
|
270
|
-
// Compute hash of package-lock.json for cache validation
|
|
271
|
-
const computePackageLockHash = async (
|
|
425
|
+
// Compute hash of package-lock.json or bun.lock for cache validation
|
|
426
|
+
const computePackageLockHash = async (
|
|
427
|
+
workingDir: string,
|
|
428
|
+
): Promise<string | null> => {
|
|
272
429
|
try {
|
|
273
|
-
|
|
274
|
-
const
|
|
275
|
-
|
|
430
|
+
// Try package-lock.json first
|
|
431
|
+
const packageLockPath = path.join(workingDir, "package-lock.json");
|
|
432
|
+
try {
|
|
433
|
+
const content = await fs.readFile(packageLockPath, "utf-8");
|
|
434
|
+
return createHash("sha256").update(content).digest("hex");
|
|
435
|
+
} catch {
|
|
436
|
+
// Try bun.lock if package-lock.json doesn't exist
|
|
437
|
+
const bunLockPath = path.join(workingDir, "bun.lock");
|
|
438
|
+
const content = await fs.readFile(bunLockPath, "utf-8");
|
|
439
|
+
return createHash("sha256").update(content).digest("hex");
|
|
440
|
+
}
|
|
276
441
|
} catch {
|
|
277
442
|
return null;
|
|
278
443
|
}
|
|
279
444
|
};
|
|
280
445
|
|
|
281
446
|
// Cache persistence functions
|
|
282
|
-
const saveCacheToDisk = async (
|
|
447
|
+
const saveCacheToDisk = async (
|
|
448
|
+
cache: ReturnType<typeof createCache>,
|
|
449
|
+
cacheDir: string,
|
|
450
|
+
workingDir: string,
|
|
451
|
+
): Promise<void> => {
|
|
283
452
|
const packageLockHash = await computePackageLockHash(workingDir);
|
|
284
|
-
|
|
453
|
+
|
|
285
454
|
const cacheData: CacheData = {
|
|
286
455
|
entries: cache.entries,
|
|
287
456
|
packages: cache.packages,
|
|
288
457
|
references: cache.referenceManager.getAllReferences(),
|
|
289
|
-
packageLockHash: packageLockHash || undefined
|
|
458
|
+
packageLockHash: packageLockHash || undefined,
|
|
290
459
|
};
|
|
291
|
-
|
|
292
|
-
const cachePath = path.join(cacheDir,
|
|
460
|
+
|
|
461
|
+
const cachePath = path.join(cacheDir, "index.json");
|
|
293
462
|
await fs.writeFile(cachePath, JSON.stringify(cacheData, null, 2));
|
|
294
463
|
};
|
|
295
464
|
|
|
296
|
-
const loadCacheFromDisk = async (
|
|
465
|
+
const loadCacheFromDisk = async (
|
|
466
|
+
cacheDir: string,
|
|
467
|
+
): Promise<CacheData | null> => {
|
|
297
468
|
try {
|
|
298
|
-
const cachePath = path.join(cacheDir,
|
|
299
|
-
const content = await fs.readFile(cachePath,
|
|
469
|
+
const cachePath = path.join(cacheDir, "index.json");
|
|
470
|
+
const content = await fs.readFile(cachePath, "utf-8");
|
|
300
471
|
return JSON.parse(content) as CacheData;
|
|
301
472
|
} catch {
|
|
302
473
|
return null;
|
|
@@ -304,29 +475,57 @@ const loadCacheFromDisk = async (cacheDir: string): Promise<CacheData | null> =>
|
|
|
304
475
|
};
|
|
305
476
|
|
|
306
477
|
// Package management functions
|
|
307
|
-
const installPackages = async (
|
|
478
|
+
const installPackages = async (
|
|
479
|
+
packages: string[],
|
|
480
|
+
workingDir: string,
|
|
481
|
+
registry?: string,
|
|
482
|
+
): Promise<void> => {
|
|
308
483
|
await ensureDir(workingDir);
|
|
309
|
-
|
|
484
|
+
|
|
310
485
|
// Check if package.json exists
|
|
311
|
-
const packageJsonPath = path.join(workingDir,
|
|
486
|
+
const packageJsonPath = path.join(workingDir, "package.json");
|
|
312
487
|
if (!(await fileExists(packageJsonPath))) {
|
|
313
488
|
// Create minimal package.json
|
|
314
489
|
const minimalPackageJson = {
|
|
315
490
|
name: "fhir-canonical-manager-workspace",
|
|
316
491
|
version: "1.0.0",
|
|
317
492
|
private: true,
|
|
318
|
-
dependencies: {}
|
|
493
|
+
dependencies: {},
|
|
319
494
|
};
|
|
320
|
-
await fs.writeFile(
|
|
495
|
+
await fs.writeFile(
|
|
496
|
+
packageJsonPath,
|
|
497
|
+
JSON.stringify(minimalPackageJson, null, 2),
|
|
498
|
+
);
|
|
321
499
|
}
|
|
322
|
-
|
|
500
|
+
|
|
501
|
+
// Detect available package manager
|
|
502
|
+
const packageManager = await detectPackageManager();
|
|
503
|
+
if (!packageManager) {
|
|
504
|
+
throw new Error("No package manager found. Please install npm or bun.");
|
|
505
|
+
}
|
|
506
|
+
|
|
323
507
|
// Install packages
|
|
324
508
|
for (const pkg of packages) {
|
|
325
509
|
try {
|
|
326
|
-
if (
|
|
327
|
-
|
|
510
|
+
if (packageManager === "bun") {
|
|
511
|
+
// Use bun with auth bypass trick for FHIR registry
|
|
512
|
+
const env = {
|
|
513
|
+
HOME: workingDir, // Prevent reading user's .npmrc
|
|
514
|
+
NPM_CONFIG_USERCONFIG: "/dev/null", // Extra safety
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
const cmd = registry
|
|
518
|
+
? `cd ${workingDir} && bun add ${pkg} --registry ${registry}`
|
|
519
|
+
: `cd ${workingDir} && bun add ${pkg}`;
|
|
520
|
+
|
|
521
|
+
await $`${cmd}`.env(env);
|
|
328
522
|
} else {
|
|
329
|
-
|
|
523
|
+
// Use npm (handles auth correctly)
|
|
524
|
+
const cmd = registry
|
|
525
|
+
? `cd ${workingDir} && npm add ${pkg} --registry ${registry}`
|
|
526
|
+
: `cd ${workingDir} && npm add ${pkg}`;
|
|
527
|
+
|
|
528
|
+
await $`${cmd}`;
|
|
330
529
|
}
|
|
331
530
|
} catch (err) {
|
|
332
531
|
console.error(`Failed to install package ${pkg}:`, err);
|
|
@@ -339,50 +538,50 @@ const installPackages = async (packages: string[], workingDir: string, registry?
|
|
|
339
538
|
const processIndex = async (
|
|
340
539
|
basePath: string,
|
|
341
540
|
packageJson: PackageJson,
|
|
342
|
-
cache: ReturnType<typeof createCache
|
|
541
|
+
cache: ReturnType<typeof createCache>,
|
|
343
542
|
): Promise<void> => {
|
|
344
|
-
const indexPath = path.join(basePath,
|
|
345
|
-
|
|
543
|
+
const indexPath = path.join(basePath, ".index.json");
|
|
544
|
+
|
|
346
545
|
try {
|
|
347
|
-
const indexContent = await fs.readFile(indexPath,
|
|
546
|
+
const indexContent = await fs.readFile(indexPath, "utf-8");
|
|
348
547
|
const index = parseIndex(indexContent, indexPath);
|
|
349
|
-
|
|
548
|
+
|
|
350
549
|
if (!index) return;
|
|
351
|
-
|
|
550
|
+
|
|
352
551
|
for (const file of index.files) {
|
|
353
552
|
if (!file.url) continue;
|
|
354
|
-
|
|
553
|
+
|
|
355
554
|
const filePath = path.join(basePath, file.filename);
|
|
356
|
-
|
|
555
|
+
|
|
357
556
|
const id = cache.referenceManager.generateId({
|
|
358
557
|
packageName: packageJson.name,
|
|
359
558
|
packageVersion: packageJson.version,
|
|
360
|
-
filePath
|
|
559
|
+
filePath,
|
|
361
560
|
});
|
|
362
|
-
|
|
561
|
+
|
|
363
562
|
cache.referenceManager.set(id, {
|
|
364
563
|
packageName: packageJson.name,
|
|
365
564
|
packageVersion: packageJson.version,
|
|
366
565
|
filePath,
|
|
367
566
|
resourceType: file.resourceType,
|
|
368
567
|
url: file.url,
|
|
369
|
-
version: file.version
|
|
568
|
+
version: file.version,
|
|
370
569
|
});
|
|
371
|
-
|
|
570
|
+
|
|
372
571
|
const entry: IndexEntry = {
|
|
373
572
|
id,
|
|
374
573
|
resourceType: file.resourceType,
|
|
375
|
-
indexVersion: index[
|
|
574
|
+
indexVersion: index["index-version"],
|
|
376
575
|
url: file.url,
|
|
377
576
|
version: file.version,
|
|
378
577
|
kind: file.kind,
|
|
379
578
|
type: file.type,
|
|
380
579
|
package: {
|
|
381
580
|
name: packageJson.name,
|
|
382
|
-
version: packageJson.version
|
|
383
|
-
}
|
|
581
|
+
version: packageJson.version,
|
|
582
|
+
},
|
|
384
583
|
};
|
|
385
|
-
|
|
584
|
+
|
|
386
585
|
if (!cache.entries[file.url]) {
|
|
387
586
|
cache.entries[file.url] = [];
|
|
388
587
|
}
|
|
@@ -398,25 +597,25 @@ const processIndex = async (
|
|
|
398
597
|
|
|
399
598
|
const scanPackage = async (
|
|
400
599
|
packagePath: string,
|
|
401
|
-
cache: ReturnType<typeof createCache
|
|
600
|
+
cache: ReturnType<typeof createCache>,
|
|
402
601
|
): Promise<void> => {
|
|
403
602
|
try {
|
|
404
|
-
const packageJsonPath = path.join(packagePath,
|
|
405
|
-
const packageJsonContent = await fs.readFile(packageJsonPath,
|
|
603
|
+
const packageJsonPath = path.join(packagePath, "package.json");
|
|
604
|
+
const packageJsonContent = await fs.readFile(packageJsonPath, "utf-8");
|
|
406
605
|
const packageJson: PackageJson = JSON.parse(packageJsonContent);
|
|
407
|
-
|
|
606
|
+
|
|
408
607
|
const packageInfo: PackageInfo = {
|
|
409
608
|
id: { name: packageJson.name, version: packageJson.version },
|
|
410
609
|
path: packagePath,
|
|
411
610
|
canonical: packageJson.canonical,
|
|
412
|
-
fhirVersions: packageJson.fhirVersions
|
|
611
|
+
fhirVersions: packageJson.fhirVersions,
|
|
413
612
|
};
|
|
414
613
|
cache.packages[packageJson.name] = packageInfo;
|
|
415
|
-
|
|
614
|
+
|
|
416
615
|
await processIndex(packagePath, packageJson, cache);
|
|
417
|
-
|
|
418
|
-
const examplesPath = path.join(packagePath,
|
|
419
|
-
if (await fileExists(path.join(examplesPath,
|
|
616
|
+
|
|
617
|
+
const examplesPath = path.join(packagePath, "examples");
|
|
618
|
+
if (await fileExists(path.join(examplesPath, ".index.json"))) {
|
|
420
619
|
await processIndex(examplesPath, packageJson, cache);
|
|
421
620
|
}
|
|
422
621
|
} catch {
|
|
@@ -426,21 +625,23 @@ const scanPackage = async (
|
|
|
426
625
|
|
|
427
626
|
const scanDirectory = async (
|
|
428
627
|
dirPath: string,
|
|
429
|
-
cache: ReturnType<typeof createCache
|
|
628
|
+
cache: ReturnType<typeof createCache>,
|
|
430
629
|
): Promise<void> => {
|
|
431
630
|
try {
|
|
432
631
|
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
433
|
-
|
|
632
|
+
|
|
434
633
|
for (const entry of entries) {
|
|
435
634
|
if (!entry.isDirectory()) continue;
|
|
436
|
-
|
|
635
|
+
|
|
437
636
|
const fullPath = path.join(dirPath, entry.name);
|
|
438
|
-
|
|
439
|
-
if (entry.name.startsWith(
|
|
440
|
-
const scopedEntries = await fs.readdir(fullPath, {
|
|
637
|
+
|
|
638
|
+
if (entry.name.startsWith("@")) {
|
|
639
|
+
const scopedEntries = await fs.readdir(fullPath, {
|
|
640
|
+
withFileTypes: true,
|
|
641
|
+
});
|
|
441
642
|
for (const scopedEntry of scopedEntries) {
|
|
442
643
|
if (!scopedEntry.isDirectory()) continue;
|
|
443
|
-
|
|
644
|
+
|
|
444
645
|
const scopedPath = path.join(fullPath, scopedEntry.name);
|
|
445
646
|
if (await isFhirPackage(scopedPath)) {
|
|
446
647
|
await scanPackage(scopedPath, cache);
|
|
@@ -460,13 +661,13 @@ const resolveWithContext = async (
|
|
|
460
661
|
url: string,
|
|
461
662
|
context: SourceContext,
|
|
462
663
|
cache: ReturnType<typeof createCache>,
|
|
463
|
-
resolveEntry: (url: string, options?: any) => Promise<IndexEntry
|
|
664
|
+
resolveEntry: (url: string, options?: any) => Promise<IndexEntry>,
|
|
464
665
|
): Promise<IndexEntry | null> => {
|
|
465
666
|
if (context.package) {
|
|
466
667
|
try {
|
|
467
668
|
return await resolveEntry(url, {
|
|
468
669
|
package: context.package.name,
|
|
469
|
-
version: context.package.version
|
|
670
|
+
version: context.package.version,
|
|
470
671
|
});
|
|
471
672
|
} catch {
|
|
472
673
|
// Fall through to global resolution
|
|
@@ -476,41 +677,46 @@ const resolveWithContext = async (
|
|
|
476
677
|
};
|
|
477
678
|
|
|
478
679
|
// Default FHIR package registry
|
|
479
|
-
const DEFAULT_REGISTRY =
|
|
680
|
+
const DEFAULT_REGISTRY = "https://fs.get-ig.org/pkgs/";
|
|
480
681
|
|
|
481
682
|
// Main implementation
|
|
482
683
|
export const CanonicalManager = (config: Config): CanonicalManager => {
|
|
483
|
-
const { packages, workingDir
|
|
484
|
-
|
|
485
|
-
const
|
|
486
|
-
|
|
684
|
+
const { packages, workingDir } = config;
|
|
685
|
+
// Ensure registry URL ends with /
|
|
686
|
+
const registry = config.registry
|
|
687
|
+
? config.registry.endsWith('/') ? config.registry : `${config.registry}/`
|
|
688
|
+
: DEFAULT_REGISTRY;
|
|
689
|
+
const nodeModulesPath = path.join(workingDir, "node_modules");
|
|
690
|
+
const cacheDir = path.join(workingDir, ".fcm", "cache");
|
|
691
|
+
|
|
487
692
|
let cache = createCache();
|
|
488
693
|
let initialized = false;
|
|
489
694
|
|
|
490
695
|
const ensureInitialized = (): void => {
|
|
491
696
|
if (!initialized) {
|
|
492
|
-
throw new Error(
|
|
697
|
+
throw new Error("CanonicalManager not initialized. Call init() first.");
|
|
493
698
|
}
|
|
494
699
|
};
|
|
495
700
|
|
|
496
701
|
const init = async (): Promise<void> => {
|
|
497
702
|
if (initialized) return;
|
|
498
|
-
|
|
703
|
+
|
|
499
704
|
// Ensure directories exist
|
|
500
705
|
await ensureDir(workingDir);
|
|
501
706
|
await ensureDir(cacheDir);
|
|
502
|
-
|
|
707
|
+
|
|
503
708
|
// Get current package-lock.json hash
|
|
504
709
|
const currentPackageLockHash = await computePackageLockHash(workingDir);
|
|
505
|
-
|
|
710
|
+
|
|
506
711
|
// Try to load cache from disk
|
|
507
712
|
const cachedData = await loadCacheFromDisk(cacheDir);
|
|
508
|
-
|
|
713
|
+
|
|
509
714
|
// Check if cache is valid (exists and package-lock.json hasn't changed)
|
|
510
|
-
const cacheValid =
|
|
715
|
+
const cacheValid =
|
|
716
|
+
cachedData &&
|
|
511
717
|
cachedData.packageLockHash === currentPackageLockHash &&
|
|
512
718
|
currentPackageLockHash !== null;
|
|
513
|
-
|
|
719
|
+
|
|
514
720
|
if (cacheValid) {
|
|
515
721
|
// Restore cache from disk
|
|
516
722
|
cache.entries = cachedData.entries;
|
|
@@ -521,22 +727,22 @@ export const CanonicalManager = (config: Config): CanonicalManager => {
|
|
|
521
727
|
} else {
|
|
522
728
|
// Cache is invalid or doesn't exist - rebuild it
|
|
523
729
|
if (cachedData && cachedData.packageLockHash !== currentPackageLockHash) {
|
|
524
|
-
console.log(
|
|
730
|
+
console.log("Package dependencies have changed, rebuilding index...");
|
|
525
731
|
}
|
|
526
|
-
|
|
732
|
+
|
|
527
733
|
// Install packages if needed
|
|
528
734
|
await installPackages(packages, workingDir, registry);
|
|
529
|
-
|
|
735
|
+
|
|
530
736
|
// Clear cache before scanning
|
|
531
737
|
cache = createCache();
|
|
532
|
-
|
|
738
|
+
|
|
533
739
|
// Scan installed packages
|
|
534
740
|
await scanDirectory(nodeModulesPath, cache);
|
|
535
|
-
|
|
741
|
+
|
|
536
742
|
// Save cache to disk with current package-lock hash
|
|
537
743
|
await saveCacheToDisk(cache, cacheDir, workingDir);
|
|
538
744
|
}
|
|
539
|
-
|
|
745
|
+
|
|
540
746
|
initialized = true;
|
|
541
747
|
};
|
|
542
748
|
|
|
@@ -549,7 +755,7 @@ export const CanonicalManager = (config: Config): CanonicalManager => {
|
|
|
549
755
|
|
|
550
756
|
const getPackages = async (): Promise<PackageId[]> => {
|
|
551
757
|
ensureInitialized();
|
|
552
|
-
return Object.values(cache.packages).map(p => p.id);
|
|
758
|
+
return Object.values(cache.packages).map((p) => p.id);
|
|
553
759
|
};
|
|
554
760
|
|
|
555
761
|
const resolveEntry = async (
|
|
@@ -558,7 +764,7 @@ export const CanonicalManager = (config: Config): CanonicalManager => {
|
|
|
558
764
|
package?: string;
|
|
559
765
|
version?: string;
|
|
560
766
|
sourceContext?: SourceContext;
|
|
561
|
-
}
|
|
767
|
+
},
|
|
562
768
|
): Promise<IndexEntry> => {
|
|
563
769
|
ensureInitialized();
|
|
564
770
|
|
|
@@ -567,7 +773,7 @@ export const CanonicalManager = (config: Config): CanonicalManager => {
|
|
|
567
773
|
canonicalUrl,
|
|
568
774
|
options.sourceContext,
|
|
569
775
|
cache,
|
|
570
|
-
resolveEntry
|
|
776
|
+
resolveEntry,
|
|
571
777
|
);
|
|
572
778
|
if (contextResolved) {
|
|
573
779
|
return contextResolved;
|
|
@@ -575,23 +781,25 @@ export const CanonicalManager = (config: Config): CanonicalManager => {
|
|
|
575
781
|
}
|
|
576
782
|
|
|
577
783
|
const entries = cache.entries[canonicalUrl] || [];
|
|
578
|
-
|
|
784
|
+
|
|
579
785
|
if (entries.length === 0) {
|
|
580
786
|
throw new Error(`Cannot resolve canonical URL: ${canonicalUrl}`);
|
|
581
787
|
}
|
|
582
788
|
|
|
583
789
|
let filtered = [...entries];
|
|
584
|
-
|
|
790
|
+
|
|
585
791
|
if (options?.package) {
|
|
586
|
-
filtered = filtered.filter(e => e.package?.name === options.package);
|
|
792
|
+
filtered = filtered.filter((e) => e.package?.name === options.package);
|
|
587
793
|
}
|
|
588
|
-
|
|
794
|
+
|
|
589
795
|
if (options?.version) {
|
|
590
|
-
filtered = filtered.filter(e => e.version === options.version);
|
|
796
|
+
filtered = filtered.filter((e) => e.version === options.version);
|
|
591
797
|
}
|
|
592
798
|
|
|
593
799
|
if (filtered.length === 0) {
|
|
594
|
-
throw new Error(
|
|
800
|
+
throw new Error(
|
|
801
|
+
`No matching resource found for ${canonicalUrl} with given options`,
|
|
802
|
+
);
|
|
595
803
|
}
|
|
596
804
|
|
|
597
805
|
return filtered[0]!;
|
|
@@ -603,7 +811,7 @@ export const CanonicalManager = (config: Config): CanonicalManager => {
|
|
|
603
811
|
package?: string;
|
|
604
812
|
version?: string;
|
|
605
813
|
sourceContext?: SourceContext;
|
|
606
|
-
}
|
|
814
|
+
},
|
|
607
815
|
): Promise<Resource> => {
|
|
608
816
|
const entry = await resolveEntry(canonicalUrl, options);
|
|
609
817
|
return read(entry);
|
|
@@ -611,20 +819,20 @@ export const CanonicalManager = (config: Config): CanonicalManager => {
|
|
|
611
819
|
|
|
612
820
|
const read = async (reference: Reference): Promise<Resource> => {
|
|
613
821
|
ensureInitialized();
|
|
614
|
-
|
|
822
|
+
|
|
615
823
|
const metadata = cache.referenceManager.get(reference.id);
|
|
616
824
|
if (!metadata) {
|
|
617
825
|
throw new Error(`Invalid reference ID: ${reference.id}`);
|
|
618
826
|
}
|
|
619
827
|
|
|
620
828
|
try {
|
|
621
|
-
const content = await fs.readFile(metadata.filePath,
|
|
829
|
+
const content = await fs.readFile(metadata.filePath, "utf-8");
|
|
622
830
|
const resource = JSON.parse(content);
|
|
623
|
-
|
|
831
|
+
|
|
624
832
|
return {
|
|
625
833
|
...resource,
|
|
626
834
|
id: reference.id,
|
|
627
|
-
resourceType: reference.resourceType
|
|
835
|
+
resourceType: reference.resourceType,
|
|
628
836
|
};
|
|
629
837
|
} catch (err) {
|
|
630
838
|
throw new Error(`Failed to read resource: ${err}`);
|
|
@@ -639,9 +847,9 @@ export const CanonicalManager = (config: Config): CanonicalManager => {
|
|
|
639
847
|
package?: PackageId;
|
|
640
848
|
}): Promise<IndexEntry[]> => {
|
|
641
849
|
ensureInitialized();
|
|
642
|
-
|
|
850
|
+
|
|
643
851
|
let results: IndexEntry[] = [];
|
|
644
|
-
|
|
852
|
+
|
|
645
853
|
if (params.url) {
|
|
646
854
|
results = cache.entries[params.url] || [];
|
|
647
855
|
} else {
|
|
@@ -649,27 +857,27 @@ export const CanonicalManager = (config: Config): CanonicalManager => {
|
|
|
649
857
|
results.push(...entries);
|
|
650
858
|
}
|
|
651
859
|
}
|
|
652
|
-
|
|
860
|
+
|
|
653
861
|
if (params.kind !== undefined) {
|
|
654
|
-
results = results.filter(e => e.kind === params.kind);
|
|
862
|
+
results = results.filter((e) => e.kind === params.kind);
|
|
655
863
|
}
|
|
656
|
-
|
|
864
|
+
|
|
657
865
|
if (params.type !== undefined) {
|
|
658
|
-
results = results.filter(e => e.type === params.type);
|
|
866
|
+
results = results.filter((e) => e.type === params.type);
|
|
659
867
|
}
|
|
660
|
-
|
|
868
|
+
|
|
661
869
|
if (params.version !== undefined) {
|
|
662
|
-
results = results.filter(e => e.version === params.version);
|
|
870
|
+
results = results.filter((e) => e.version === params.version);
|
|
663
871
|
}
|
|
664
|
-
|
|
872
|
+
|
|
665
873
|
if (params.package) {
|
|
666
874
|
const pkg = params.package;
|
|
667
|
-
results = results.filter(
|
|
668
|
-
e
|
|
669
|
-
|
|
875
|
+
results = results.filter(
|
|
876
|
+
(e) =>
|
|
877
|
+
e.package?.name === pkg.name && e.package?.version === pkg.version,
|
|
670
878
|
);
|
|
671
879
|
}
|
|
672
|
-
|
|
880
|
+
|
|
673
881
|
return results;
|
|
674
882
|
};
|
|
675
883
|
|
|
@@ -681,10 +889,98 @@ export const CanonicalManager = (config: Config): CanonicalManager => {
|
|
|
681
889
|
package?: PackageId;
|
|
682
890
|
}): Promise<Resource[]> => {
|
|
683
891
|
const entries = await searchEntries(params);
|
|
684
|
-
const resources = await Promise.all(entries.map(entry => read(entry)));
|
|
892
|
+
const resources = await Promise.all(entries.map((entry) => read(entry)));
|
|
685
893
|
return resources;
|
|
686
894
|
};
|
|
687
895
|
|
|
896
|
+
const smartSearch = async (
|
|
897
|
+
searchTerms: string[],
|
|
898
|
+
filters?: {
|
|
899
|
+
resourceType?: string;
|
|
900
|
+
type?: string;
|
|
901
|
+
kind?: string;
|
|
902
|
+
package?: PackageId;
|
|
903
|
+
}
|
|
904
|
+
): Promise<IndexEntry[]> => {
|
|
905
|
+
ensureInitialized();
|
|
906
|
+
|
|
907
|
+
// Start with base search using filters
|
|
908
|
+
let results = await searchEntries({
|
|
909
|
+
kind: filters?.kind,
|
|
910
|
+
package: filters?.package,
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
// Apply resourceType filter
|
|
914
|
+
if (filters?.resourceType) {
|
|
915
|
+
results = results.filter(entry => entry.resourceType === filters.resourceType);
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
// Apply type filter
|
|
919
|
+
if (filters?.type) {
|
|
920
|
+
results = results.filter(entry => entry.type === filters.type);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
// Apply smart search terms if provided
|
|
924
|
+
if (searchTerms.length > 0) {
|
|
925
|
+
const terms = searchTerms.map(t => t.toLowerCase());
|
|
926
|
+
|
|
927
|
+
results = results.filter(entry => {
|
|
928
|
+
if (!entry.url) return false;
|
|
929
|
+
const urlLower = entry.url.toLowerCase();
|
|
930
|
+
|
|
931
|
+
// Also check type and resourceType for matching
|
|
932
|
+
const fullText = [
|
|
933
|
+
urlLower,
|
|
934
|
+
entry.type?.toLowerCase() || '',
|
|
935
|
+
entry.resourceType?.toLowerCase() || ''
|
|
936
|
+
].join(' ');
|
|
937
|
+
|
|
938
|
+
// Check if all search terms match
|
|
939
|
+
return terms.every(term => {
|
|
940
|
+
// Split the text into parts (by /, -, _, ., spaces)
|
|
941
|
+
const allParts = fullText.split(/[\/\-_\.\s]+/);
|
|
942
|
+
|
|
943
|
+
// Check if any part starts with the search term
|
|
944
|
+
const directMatch = allParts.some(part => part.startsWith(term));
|
|
945
|
+
if (directMatch) return true;
|
|
946
|
+
|
|
947
|
+
// Smart matching for common abbreviations
|
|
948
|
+
const expandedTerms: Record<string, string[]> = {
|
|
949
|
+
'str': ['structure'],
|
|
950
|
+
'struct': ['structure'],
|
|
951
|
+
'def': ['definition'],
|
|
952
|
+
'pati': ['patient'],
|
|
953
|
+
'obs': ['observation'],
|
|
954
|
+
'org': ['organization'],
|
|
955
|
+
'pract': ['practitioner'],
|
|
956
|
+
'med': ['medication', 'medicinal'],
|
|
957
|
+
'req': ['request'],
|
|
958
|
+
'resp': ['response'],
|
|
959
|
+
'ref': ['reference'],
|
|
960
|
+
'val': ['value'],
|
|
961
|
+
'code': ['codesystem', 'code'],
|
|
962
|
+
'cs': ['codesystem'],
|
|
963
|
+
'vs': ['valueset'],
|
|
964
|
+
'sd': ['structuredefinition'],
|
|
965
|
+
};
|
|
966
|
+
|
|
967
|
+
// Check if the term is an abbreviation
|
|
968
|
+
const expansions = expandedTerms[term] || [];
|
|
969
|
+
for (const expansion of expansions) {
|
|
970
|
+
if (allParts.some(part => part.startsWith(expansion))) {
|
|
971
|
+
return true;
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
// If still no match, check if the term appears anywhere (substring match)
|
|
976
|
+
return fullText.includes(term);
|
|
977
|
+
});
|
|
978
|
+
});
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
return results;
|
|
982
|
+
};
|
|
983
|
+
|
|
688
984
|
return {
|
|
689
985
|
init,
|
|
690
986
|
destroy,
|
|
@@ -693,9 +989,10 @@ export const CanonicalManager = (config: Config): CanonicalManager => {
|
|
|
693
989
|
resolve,
|
|
694
990
|
read,
|
|
695
991
|
searchEntries,
|
|
696
|
-
search
|
|
992
|
+
search,
|
|
993
|
+
smartSearch,
|
|
697
994
|
};
|
|
698
995
|
};
|
|
699
996
|
|
|
700
997
|
// Default export
|
|
701
|
-
export default CanonicalManager;
|
|
998
|
+
export default CanonicalManager;
|