@atomic-ehr/fhir-canonical-manager 0.0.7 → 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 +1 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +44 -36
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/init.d.ts.map +1 -1
- package/dist/cli/init.js +18 -12
- 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 +7 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +142 -79
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/src/cli/index.ts +52 -42
- package/src/cli/init.ts +30 -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 +311 -181
package/src/index.ts
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
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 { exec } from
|
|
10
|
-
import { promisify } from
|
|
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
11
|
|
|
12
12
|
// Shell command utilities
|
|
13
13
|
const execAsync = promisify(exec);
|
|
@@ -18,9 +18,14 @@ class ShellError extends Error {
|
|
|
18
18
|
stdout: string;
|
|
19
19
|
stderr: string;
|
|
20
20
|
|
|
21
|
-
constructor(
|
|
21
|
+
constructor(
|
|
22
|
+
message: string,
|
|
23
|
+
exitCode: number,
|
|
24
|
+
stdout: string,
|
|
25
|
+
stderr: string,
|
|
26
|
+
) {
|
|
22
27
|
super(message);
|
|
23
|
-
this.name =
|
|
28
|
+
this.name = "ShellError";
|
|
24
29
|
this.exitCode = exitCode;
|
|
25
30
|
this.stdout = stdout;
|
|
26
31
|
this.stderr = stderr;
|
|
@@ -43,8 +48,8 @@ interface ShellPromise extends Promise<ShellResult> {
|
|
|
43
48
|
*/
|
|
44
49
|
function $(strings: TemplateStringsArray, ...values: any[]): ShellPromise {
|
|
45
50
|
const command = strings.reduce((acc, str, i) => {
|
|
46
|
-
return acc + str + (values[i] ||
|
|
47
|
-
},
|
|
51
|
+
return acc + str + (values[i] || "");
|
|
52
|
+
}, "");
|
|
48
53
|
|
|
49
54
|
let envVars: Record<string, string> = {};
|
|
50
55
|
|
|
@@ -55,38 +60,33 @@ function $(strings: TemplateStringsArray, ...values: any[]): ShellPromise {
|
|
|
55
60
|
shell: true,
|
|
56
61
|
maxBuffer: 10 * 1024 * 1024, // 10MB buffer
|
|
57
62
|
};
|
|
58
|
-
|
|
63
|
+
|
|
59
64
|
// Apply environment variables if set
|
|
60
65
|
if (Object.keys(envVars).length > 0) {
|
|
61
66
|
execOptions.env = { ...process.env, ...envVars };
|
|
62
67
|
}
|
|
63
|
-
|
|
68
|
+
|
|
64
69
|
const { stdout, stderr } = await execAsync(command, execOptions);
|
|
65
|
-
|
|
70
|
+
|
|
66
71
|
return {
|
|
67
|
-
stdout: stdout?.toString() ||
|
|
68
|
-
stderr: stderr?.toString() ||
|
|
69
|
-
exitCode: 0
|
|
72
|
+
stdout: stdout?.toString() || "",
|
|
73
|
+
stderr: stderr?.toString() || "",
|
|
74
|
+
exitCode: 0,
|
|
70
75
|
};
|
|
71
76
|
} catch (error: any) {
|
|
72
77
|
// Extract error details
|
|
73
78
|
const code = error.code || 1;
|
|
74
|
-
const stdout = error.stdout?.toString() ||
|
|
75
|
-
const stderr = error.stderr?.toString() || error.message ||
|
|
76
|
-
|
|
77
|
-
throw new ShellError(
|
|
78
|
-
`Command failed: ${command}`,
|
|
79
|
-
code,
|
|
80
|
-
stdout,
|
|
81
|
-
stderr
|
|
82
|
-
);
|
|
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
83
|
}
|
|
84
84
|
};
|
|
85
85
|
|
|
86
86
|
// Create a lazy promise that only executes when awaited
|
|
87
87
|
let executed = false;
|
|
88
88
|
let resultPromise: Promise<ShellResult> | null = null;
|
|
89
|
-
|
|
89
|
+
|
|
90
90
|
const lazyPromise = {
|
|
91
91
|
then(onFulfilled?: any, onRejected?: any) {
|
|
92
92
|
if (!executed) {
|
|
@@ -107,7 +107,7 @@ function $(strings: TemplateStringsArray, ...values: any[]): ShellPromise {
|
|
|
107
107
|
(reason: any) => {
|
|
108
108
|
onFinally?.();
|
|
109
109
|
throw reason;
|
|
110
|
-
}
|
|
110
|
+
},
|
|
111
111
|
);
|
|
112
112
|
},
|
|
113
113
|
quiet() {
|
|
@@ -118,25 +118,25 @@ function $(strings: TemplateStringsArray, ...values: any[]): ShellPromise {
|
|
|
118
118
|
return this;
|
|
119
119
|
},
|
|
120
120
|
// Add Symbol.toStringTag to satisfy Promise interface
|
|
121
|
-
[Symbol.toStringTag]:
|
|
121
|
+
[Symbol.toStringTag]: "ShellPromise" as const,
|
|
122
122
|
};
|
|
123
|
-
|
|
123
|
+
|
|
124
124
|
return lazyPromise as any as ShellPromise;
|
|
125
125
|
}
|
|
126
126
|
|
|
127
127
|
/**
|
|
128
128
|
* Detect available package manager
|
|
129
129
|
*/
|
|
130
|
-
async function detectPackageManager(): Promise<
|
|
130
|
+
async function detectPackageManager(): Promise<"bun" | "npm" | null> {
|
|
131
131
|
try {
|
|
132
132
|
// Check for bun first
|
|
133
133
|
await $`bun --version`.quiet();
|
|
134
|
-
return
|
|
134
|
+
return "bun";
|
|
135
135
|
} catch {
|
|
136
136
|
try {
|
|
137
137
|
// Fall back to npm
|
|
138
138
|
await $`npm --version`.quiet();
|
|
139
|
-
return
|
|
139
|
+
return "npm";
|
|
140
140
|
} catch {
|
|
141
141
|
return null;
|
|
142
142
|
}
|
|
@@ -189,36 +189,51 @@ export interface CanonicalManager {
|
|
|
189
189
|
init(): Promise<void>;
|
|
190
190
|
destroy(): Promise<void>;
|
|
191
191
|
packages(): Promise<PackageId[]>;
|
|
192
|
-
resolveEntry(
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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>;
|
|
202
208
|
read(reference: Reference): Promise<Resource>;
|
|
203
209
|
searchEntries(params: {
|
|
204
|
-
kind?: string
|
|
205
|
-
url?: string
|
|
206
|
-
type?: string
|
|
207
|
-
version?: string
|
|
208
|
-
package?: PackageId
|
|
210
|
+
kind?: string;
|
|
211
|
+
url?: string;
|
|
212
|
+
type?: string;
|
|
213
|
+
version?: string;
|
|
214
|
+
package?: PackageId;
|
|
209
215
|
}): Promise<IndexEntry[]>;
|
|
210
216
|
search(params: {
|
|
211
|
-
kind?: string
|
|
212
|
-
url?: string
|
|
213
|
-
type?: string
|
|
214
|
-
version?: string
|
|
215
|
-
package?: PackageId
|
|
217
|
+
kind?: string;
|
|
218
|
+
url?: string;
|
|
219
|
+
type?: string;
|
|
220
|
+
version?: string;
|
|
221
|
+
package?: PackageId;
|
|
216
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[]>;
|
|
217
232
|
}
|
|
218
233
|
|
|
219
234
|
// Internal types
|
|
220
235
|
interface IndexFile {
|
|
221
|
-
|
|
236
|
+
"index-version": number;
|
|
222
237
|
files: IndexFileEntry[];
|
|
223
238
|
}
|
|
224
239
|
|
|
@@ -287,7 +302,7 @@ const generateReferenceId = (metadata: {
|
|
|
287
302
|
filePath: string;
|
|
288
303
|
}): string => {
|
|
289
304
|
const input = `${metadata.packageName}@${metadata.packageVersion}:${metadata.filePath}`;
|
|
290
|
-
return createHash(
|
|
305
|
+
return createHash("sha256").update(input).digest("base64url");
|
|
291
306
|
};
|
|
292
307
|
|
|
293
308
|
export const ReferenceManager = (): ReferenceStore & {
|
|
@@ -313,8 +328,8 @@ export const ReferenceManager = (): ReferenceStore & {
|
|
|
313
328
|
};
|
|
314
329
|
|
|
315
330
|
const clear = (): void => {
|
|
316
|
-
Object.keys(references).forEach(key => delete references[key]);
|
|
317
|
-
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]);
|
|
318
333
|
};
|
|
319
334
|
|
|
320
335
|
return {
|
|
@@ -327,32 +342,34 @@ export const ReferenceManager = (): ReferenceStore & {
|
|
|
327
342
|
getIdsByUrl: (url: string) => urlToIds[url] || [],
|
|
328
343
|
createReference: (id: string, metadata: ReferenceMetadata): Reference => ({
|
|
329
344
|
id,
|
|
330
|
-
resourceType: metadata.resourceType
|
|
345
|
+
resourceType: metadata.resourceType,
|
|
331
346
|
}),
|
|
332
|
-
getAllReferences: () => references
|
|
347
|
+
getAllReferences: () => references,
|
|
333
348
|
};
|
|
334
349
|
};
|
|
335
350
|
|
|
336
351
|
// Parser functions
|
|
337
352
|
const isValidFileEntry = (entry: any): boolean => {
|
|
338
|
-
if (!entry || typeof entry !==
|
|
339
|
-
if (!entry.filename || typeof entry.filename !==
|
|
340
|
-
if (!entry.resourceType || typeof entry.resourceType !==
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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"];
|
|
344
360
|
for (const field of optionalStringFields) {
|
|
345
|
-
if (entry[field] !== undefined && typeof entry[field] !==
|
|
361
|
+
if (entry[field] !== undefined && typeof entry[field] !== "string") {
|
|
346
362
|
return false;
|
|
347
363
|
}
|
|
348
364
|
}
|
|
349
|
-
|
|
365
|
+
|
|
350
366
|
return true;
|
|
351
367
|
};
|
|
352
368
|
|
|
353
369
|
const isValidIndexFile = (data: any): boolean => {
|
|
354
|
-
if (!data || typeof data !==
|
|
355
|
-
if (!data[
|
|
370
|
+
if (!data || typeof data !== "object") return false;
|
|
371
|
+
if (!data["index-version"] || typeof data["index-version"] !== "number")
|
|
372
|
+
return false;
|
|
356
373
|
if (!Array.isArray(data.files)) return false;
|
|
357
374
|
return data.files.every((file: any) => isValidFileEntry(file));
|
|
358
375
|
};
|
|
@@ -388,7 +405,7 @@ const ensureDir = async (dirPath: string): Promise<void> => {
|
|
|
388
405
|
};
|
|
389
406
|
|
|
390
407
|
const isFhirPackage = async (dirPath: string): Promise<boolean> => {
|
|
391
|
-
const indexPath = path.join(dirPath,
|
|
408
|
+
const indexPath = path.join(dirPath, ".index.json");
|
|
392
409
|
return fileExists(indexPath);
|
|
393
410
|
};
|
|
394
411
|
|
|
@@ -401,23 +418,25 @@ const createCache = (): IndexCache & {
|
|
|
401
418
|
entries: {},
|
|
402
419
|
packages: {},
|
|
403
420
|
references: {},
|
|
404
|
-
referenceManager
|
|
421
|
+
referenceManager,
|
|
405
422
|
};
|
|
406
423
|
};
|
|
407
424
|
|
|
408
425
|
// Compute hash of package-lock.json or bun.lock for cache validation
|
|
409
|
-
const computePackageLockHash = async (
|
|
426
|
+
const computePackageLockHash = async (
|
|
427
|
+
workingDir: string,
|
|
428
|
+
): Promise<string | null> => {
|
|
410
429
|
try {
|
|
411
430
|
// Try package-lock.json first
|
|
412
|
-
const packageLockPath = path.join(workingDir,
|
|
431
|
+
const packageLockPath = path.join(workingDir, "package-lock.json");
|
|
413
432
|
try {
|
|
414
|
-
const content = await fs.readFile(packageLockPath,
|
|
415
|
-
return createHash(
|
|
433
|
+
const content = await fs.readFile(packageLockPath, "utf-8");
|
|
434
|
+
return createHash("sha256").update(content).digest("hex");
|
|
416
435
|
} catch {
|
|
417
436
|
// Try bun.lock if package-lock.json doesn't exist
|
|
418
|
-
const bunLockPath = path.join(workingDir,
|
|
419
|
-
const content = await fs.readFile(bunLockPath,
|
|
420
|
-
return createHash(
|
|
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");
|
|
421
440
|
}
|
|
422
441
|
} catch {
|
|
423
442
|
return null;
|
|
@@ -425,24 +444,30 @@ const computePackageLockHash = async (workingDir: string): Promise<string | null
|
|
|
425
444
|
};
|
|
426
445
|
|
|
427
446
|
// Cache persistence functions
|
|
428
|
-
const saveCacheToDisk = async (
|
|
447
|
+
const saveCacheToDisk = async (
|
|
448
|
+
cache: ReturnType<typeof createCache>,
|
|
449
|
+
cacheDir: string,
|
|
450
|
+
workingDir: string,
|
|
451
|
+
): Promise<void> => {
|
|
429
452
|
const packageLockHash = await computePackageLockHash(workingDir);
|
|
430
|
-
|
|
453
|
+
|
|
431
454
|
const cacheData: CacheData = {
|
|
432
455
|
entries: cache.entries,
|
|
433
456
|
packages: cache.packages,
|
|
434
457
|
references: cache.referenceManager.getAllReferences(),
|
|
435
|
-
packageLockHash: packageLockHash || undefined
|
|
458
|
+
packageLockHash: packageLockHash || undefined,
|
|
436
459
|
};
|
|
437
|
-
|
|
438
|
-
const cachePath = path.join(cacheDir,
|
|
460
|
+
|
|
461
|
+
const cachePath = path.join(cacheDir, "index.json");
|
|
439
462
|
await fs.writeFile(cachePath, JSON.stringify(cacheData, null, 2));
|
|
440
463
|
};
|
|
441
464
|
|
|
442
|
-
const loadCacheFromDisk = async (
|
|
465
|
+
const loadCacheFromDisk = async (
|
|
466
|
+
cacheDir: string,
|
|
467
|
+
): Promise<CacheData | null> => {
|
|
443
468
|
try {
|
|
444
|
-
const cachePath = path.join(cacheDir,
|
|
445
|
-
const content = await fs.readFile(cachePath,
|
|
469
|
+
const cachePath = path.join(cacheDir, "index.json");
|
|
470
|
+
const content = await fs.readFile(cachePath, "utf-8");
|
|
446
471
|
return JSON.parse(content) as CacheData;
|
|
447
472
|
} catch {
|
|
448
473
|
return null;
|
|
@@ -450,49 +475,56 @@ const loadCacheFromDisk = async (cacheDir: string): Promise<CacheData | null> =>
|
|
|
450
475
|
};
|
|
451
476
|
|
|
452
477
|
// Package management functions
|
|
453
|
-
const installPackages = async (
|
|
478
|
+
const installPackages = async (
|
|
479
|
+
packages: string[],
|
|
480
|
+
workingDir: string,
|
|
481
|
+
registry?: string,
|
|
482
|
+
): Promise<void> => {
|
|
454
483
|
await ensureDir(workingDir);
|
|
455
|
-
|
|
484
|
+
|
|
456
485
|
// Check if package.json exists
|
|
457
|
-
const packageJsonPath = path.join(workingDir,
|
|
486
|
+
const packageJsonPath = path.join(workingDir, "package.json");
|
|
458
487
|
if (!(await fileExists(packageJsonPath))) {
|
|
459
488
|
// Create minimal package.json
|
|
460
489
|
const minimalPackageJson = {
|
|
461
490
|
name: "fhir-canonical-manager-workspace",
|
|
462
491
|
version: "1.0.0",
|
|
463
492
|
private: true,
|
|
464
|
-
dependencies: {}
|
|
493
|
+
dependencies: {},
|
|
465
494
|
};
|
|
466
|
-
await fs.writeFile(
|
|
495
|
+
await fs.writeFile(
|
|
496
|
+
packageJsonPath,
|
|
497
|
+
JSON.stringify(minimalPackageJson, null, 2),
|
|
498
|
+
);
|
|
467
499
|
}
|
|
468
|
-
|
|
500
|
+
|
|
469
501
|
// Detect available package manager
|
|
470
502
|
const packageManager = await detectPackageManager();
|
|
471
503
|
if (!packageManager) {
|
|
472
|
-
throw new Error(
|
|
504
|
+
throw new Error("No package manager found. Please install npm or bun.");
|
|
473
505
|
}
|
|
474
|
-
|
|
506
|
+
|
|
475
507
|
// Install packages
|
|
476
508
|
for (const pkg of packages) {
|
|
477
509
|
try {
|
|
478
|
-
if (packageManager ===
|
|
510
|
+
if (packageManager === "bun") {
|
|
479
511
|
// Use bun with auth bypass trick for FHIR registry
|
|
480
512
|
const env = {
|
|
481
|
-
HOME: workingDir,
|
|
482
|
-
NPM_CONFIG_USERCONFIG:
|
|
513
|
+
HOME: workingDir, // Prevent reading user's .npmrc
|
|
514
|
+
NPM_CONFIG_USERCONFIG: "/dev/null", // Extra safety
|
|
483
515
|
};
|
|
484
|
-
|
|
485
|
-
const cmd = registry
|
|
516
|
+
|
|
517
|
+
const cmd = registry
|
|
486
518
|
? `cd ${workingDir} && bun add ${pkg} --registry ${registry}`
|
|
487
519
|
: `cd ${workingDir} && bun add ${pkg}`;
|
|
488
|
-
|
|
520
|
+
|
|
489
521
|
await $`${cmd}`.env(env);
|
|
490
522
|
} else {
|
|
491
523
|
// Use npm (handles auth correctly)
|
|
492
524
|
const cmd = registry
|
|
493
525
|
? `cd ${workingDir} && npm add ${pkg} --registry ${registry}`
|
|
494
526
|
: `cd ${workingDir} && npm add ${pkg}`;
|
|
495
|
-
|
|
527
|
+
|
|
496
528
|
await $`${cmd}`;
|
|
497
529
|
}
|
|
498
530
|
} catch (err) {
|
|
@@ -506,50 +538,50 @@ const installPackages = async (packages: string[], workingDir: string, registry?
|
|
|
506
538
|
const processIndex = async (
|
|
507
539
|
basePath: string,
|
|
508
540
|
packageJson: PackageJson,
|
|
509
|
-
cache: ReturnType<typeof createCache
|
|
541
|
+
cache: ReturnType<typeof createCache>,
|
|
510
542
|
): Promise<void> => {
|
|
511
|
-
const indexPath = path.join(basePath,
|
|
512
|
-
|
|
543
|
+
const indexPath = path.join(basePath, ".index.json");
|
|
544
|
+
|
|
513
545
|
try {
|
|
514
|
-
const indexContent = await fs.readFile(indexPath,
|
|
546
|
+
const indexContent = await fs.readFile(indexPath, "utf-8");
|
|
515
547
|
const index = parseIndex(indexContent, indexPath);
|
|
516
|
-
|
|
548
|
+
|
|
517
549
|
if (!index) return;
|
|
518
|
-
|
|
550
|
+
|
|
519
551
|
for (const file of index.files) {
|
|
520
552
|
if (!file.url) continue;
|
|
521
|
-
|
|
553
|
+
|
|
522
554
|
const filePath = path.join(basePath, file.filename);
|
|
523
|
-
|
|
555
|
+
|
|
524
556
|
const id = cache.referenceManager.generateId({
|
|
525
557
|
packageName: packageJson.name,
|
|
526
558
|
packageVersion: packageJson.version,
|
|
527
|
-
filePath
|
|
559
|
+
filePath,
|
|
528
560
|
});
|
|
529
|
-
|
|
561
|
+
|
|
530
562
|
cache.referenceManager.set(id, {
|
|
531
563
|
packageName: packageJson.name,
|
|
532
564
|
packageVersion: packageJson.version,
|
|
533
565
|
filePath,
|
|
534
566
|
resourceType: file.resourceType,
|
|
535
567
|
url: file.url,
|
|
536
|
-
version: file.version
|
|
568
|
+
version: file.version,
|
|
537
569
|
});
|
|
538
|
-
|
|
570
|
+
|
|
539
571
|
const entry: IndexEntry = {
|
|
540
572
|
id,
|
|
541
573
|
resourceType: file.resourceType,
|
|
542
|
-
indexVersion: index[
|
|
574
|
+
indexVersion: index["index-version"],
|
|
543
575
|
url: file.url,
|
|
544
576
|
version: file.version,
|
|
545
577
|
kind: file.kind,
|
|
546
578
|
type: file.type,
|
|
547
579
|
package: {
|
|
548
580
|
name: packageJson.name,
|
|
549
|
-
version: packageJson.version
|
|
550
|
-
}
|
|
581
|
+
version: packageJson.version,
|
|
582
|
+
},
|
|
551
583
|
};
|
|
552
|
-
|
|
584
|
+
|
|
553
585
|
if (!cache.entries[file.url]) {
|
|
554
586
|
cache.entries[file.url] = [];
|
|
555
587
|
}
|
|
@@ -565,25 +597,25 @@ const processIndex = async (
|
|
|
565
597
|
|
|
566
598
|
const scanPackage = async (
|
|
567
599
|
packagePath: string,
|
|
568
|
-
cache: ReturnType<typeof createCache
|
|
600
|
+
cache: ReturnType<typeof createCache>,
|
|
569
601
|
): Promise<void> => {
|
|
570
602
|
try {
|
|
571
|
-
const packageJsonPath = path.join(packagePath,
|
|
572
|
-
const packageJsonContent = await fs.readFile(packageJsonPath,
|
|
603
|
+
const packageJsonPath = path.join(packagePath, "package.json");
|
|
604
|
+
const packageJsonContent = await fs.readFile(packageJsonPath, "utf-8");
|
|
573
605
|
const packageJson: PackageJson = JSON.parse(packageJsonContent);
|
|
574
|
-
|
|
606
|
+
|
|
575
607
|
const packageInfo: PackageInfo = {
|
|
576
608
|
id: { name: packageJson.name, version: packageJson.version },
|
|
577
609
|
path: packagePath,
|
|
578
610
|
canonical: packageJson.canonical,
|
|
579
|
-
fhirVersions: packageJson.fhirVersions
|
|
611
|
+
fhirVersions: packageJson.fhirVersions,
|
|
580
612
|
};
|
|
581
613
|
cache.packages[packageJson.name] = packageInfo;
|
|
582
|
-
|
|
614
|
+
|
|
583
615
|
await processIndex(packagePath, packageJson, cache);
|
|
584
|
-
|
|
585
|
-
const examplesPath = path.join(packagePath,
|
|
586
|
-
if (await fileExists(path.join(examplesPath,
|
|
616
|
+
|
|
617
|
+
const examplesPath = path.join(packagePath, "examples");
|
|
618
|
+
if (await fileExists(path.join(examplesPath, ".index.json"))) {
|
|
587
619
|
await processIndex(examplesPath, packageJson, cache);
|
|
588
620
|
}
|
|
589
621
|
} catch {
|
|
@@ -593,21 +625,23 @@ const scanPackage = async (
|
|
|
593
625
|
|
|
594
626
|
const scanDirectory = async (
|
|
595
627
|
dirPath: string,
|
|
596
|
-
cache: ReturnType<typeof createCache
|
|
628
|
+
cache: ReturnType<typeof createCache>,
|
|
597
629
|
): Promise<void> => {
|
|
598
630
|
try {
|
|
599
631
|
const entries = await fs.readdir(dirPath, { withFileTypes: true });
|
|
600
|
-
|
|
632
|
+
|
|
601
633
|
for (const entry of entries) {
|
|
602
634
|
if (!entry.isDirectory()) continue;
|
|
603
|
-
|
|
635
|
+
|
|
604
636
|
const fullPath = path.join(dirPath, entry.name);
|
|
605
|
-
|
|
606
|
-
if (entry.name.startsWith(
|
|
607
|
-
const scopedEntries = await fs.readdir(fullPath, {
|
|
637
|
+
|
|
638
|
+
if (entry.name.startsWith("@")) {
|
|
639
|
+
const scopedEntries = await fs.readdir(fullPath, {
|
|
640
|
+
withFileTypes: true,
|
|
641
|
+
});
|
|
608
642
|
for (const scopedEntry of scopedEntries) {
|
|
609
643
|
if (!scopedEntry.isDirectory()) continue;
|
|
610
|
-
|
|
644
|
+
|
|
611
645
|
const scopedPath = path.join(fullPath, scopedEntry.name);
|
|
612
646
|
if (await isFhirPackage(scopedPath)) {
|
|
613
647
|
await scanPackage(scopedPath, cache);
|
|
@@ -627,13 +661,13 @@ const resolveWithContext = async (
|
|
|
627
661
|
url: string,
|
|
628
662
|
context: SourceContext,
|
|
629
663
|
cache: ReturnType<typeof createCache>,
|
|
630
|
-
resolveEntry: (url: string, options?: any) => Promise<IndexEntry
|
|
664
|
+
resolveEntry: (url: string, options?: any) => Promise<IndexEntry>,
|
|
631
665
|
): Promise<IndexEntry | null> => {
|
|
632
666
|
if (context.package) {
|
|
633
667
|
try {
|
|
634
668
|
return await resolveEntry(url, {
|
|
635
669
|
package: context.package.name,
|
|
636
|
-
version: context.package.version
|
|
670
|
+
version: context.package.version,
|
|
637
671
|
});
|
|
638
672
|
} catch {
|
|
639
673
|
// Fall through to global resolution
|
|
@@ -643,41 +677,46 @@ const resolveWithContext = async (
|
|
|
643
677
|
};
|
|
644
678
|
|
|
645
679
|
// Default FHIR package registry
|
|
646
|
-
const DEFAULT_REGISTRY =
|
|
680
|
+
const DEFAULT_REGISTRY = "https://fs.get-ig.org/pkgs/";
|
|
647
681
|
|
|
648
682
|
// Main implementation
|
|
649
683
|
export const CanonicalManager = (config: Config): CanonicalManager => {
|
|
650
|
-
const { packages, workingDir
|
|
651
|
-
|
|
652
|
-
const
|
|
653
|
-
|
|
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
|
+
|
|
654
692
|
let cache = createCache();
|
|
655
693
|
let initialized = false;
|
|
656
694
|
|
|
657
695
|
const ensureInitialized = (): void => {
|
|
658
696
|
if (!initialized) {
|
|
659
|
-
throw new Error(
|
|
697
|
+
throw new Error("CanonicalManager not initialized. Call init() first.");
|
|
660
698
|
}
|
|
661
699
|
};
|
|
662
700
|
|
|
663
701
|
const init = async (): Promise<void> => {
|
|
664
702
|
if (initialized) return;
|
|
665
|
-
|
|
703
|
+
|
|
666
704
|
// Ensure directories exist
|
|
667
705
|
await ensureDir(workingDir);
|
|
668
706
|
await ensureDir(cacheDir);
|
|
669
|
-
|
|
707
|
+
|
|
670
708
|
// Get current package-lock.json hash
|
|
671
709
|
const currentPackageLockHash = await computePackageLockHash(workingDir);
|
|
672
|
-
|
|
710
|
+
|
|
673
711
|
// Try to load cache from disk
|
|
674
712
|
const cachedData = await loadCacheFromDisk(cacheDir);
|
|
675
|
-
|
|
713
|
+
|
|
676
714
|
// Check if cache is valid (exists and package-lock.json hasn't changed)
|
|
677
|
-
const cacheValid =
|
|
715
|
+
const cacheValid =
|
|
716
|
+
cachedData &&
|
|
678
717
|
cachedData.packageLockHash === currentPackageLockHash &&
|
|
679
718
|
currentPackageLockHash !== null;
|
|
680
|
-
|
|
719
|
+
|
|
681
720
|
if (cacheValid) {
|
|
682
721
|
// Restore cache from disk
|
|
683
722
|
cache.entries = cachedData.entries;
|
|
@@ -688,22 +727,22 @@ export const CanonicalManager = (config: Config): CanonicalManager => {
|
|
|
688
727
|
} else {
|
|
689
728
|
// Cache is invalid or doesn't exist - rebuild it
|
|
690
729
|
if (cachedData && cachedData.packageLockHash !== currentPackageLockHash) {
|
|
691
|
-
console.log(
|
|
730
|
+
console.log("Package dependencies have changed, rebuilding index...");
|
|
692
731
|
}
|
|
693
|
-
|
|
732
|
+
|
|
694
733
|
// Install packages if needed
|
|
695
734
|
await installPackages(packages, workingDir, registry);
|
|
696
|
-
|
|
735
|
+
|
|
697
736
|
// Clear cache before scanning
|
|
698
737
|
cache = createCache();
|
|
699
|
-
|
|
738
|
+
|
|
700
739
|
// Scan installed packages
|
|
701
740
|
await scanDirectory(nodeModulesPath, cache);
|
|
702
|
-
|
|
741
|
+
|
|
703
742
|
// Save cache to disk with current package-lock hash
|
|
704
743
|
await saveCacheToDisk(cache, cacheDir, workingDir);
|
|
705
744
|
}
|
|
706
|
-
|
|
745
|
+
|
|
707
746
|
initialized = true;
|
|
708
747
|
};
|
|
709
748
|
|
|
@@ -716,7 +755,7 @@ export const CanonicalManager = (config: Config): CanonicalManager => {
|
|
|
716
755
|
|
|
717
756
|
const getPackages = async (): Promise<PackageId[]> => {
|
|
718
757
|
ensureInitialized();
|
|
719
|
-
return Object.values(cache.packages).map(p => p.id);
|
|
758
|
+
return Object.values(cache.packages).map((p) => p.id);
|
|
720
759
|
};
|
|
721
760
|
|
|
722
761
|
const resolveEntry = async (
|
|
@@ -725,7 +764,7 @@ export const CanonicalManager = (config: Config): CanonicalManager => {
|
|
|
725
764
|
package?: string;
|
|
726
765
|
version?: string;
|
|
727
766
|
sourceContext?: SourceContext;
|
|
728
|
-
}
|
|
767
|
+
},
|
|
729
768
|
): Promise<IndexEntry> => {
|
|
730
769
|
ensureInitialized();
|
|
731
770
|
|
|
@@ -734,7 +773,7 @@ export const CanonicalManager = (config: Config): CanonicalManager => {
|
|
|
734
773
|
canonicalUrl,
|
|
735
774
|
options.sourceContext,
|
|
736
775
|
cache,
|
|
737
|
-
resolveEntry
|
|
776
|
+
resolveEntry,
|
|
738
777
|
);
|
|
739
778
|
if (contextResolved) {
|
|
740
779
|
return contextResolved;
|
|
@@ -742,23 +781,25 @@ export const CanonicalManager = (config: Config): CanonicalManager => {
|
|
|
742
781
|
}
|
|
743
782
|
|
|
744
783
|
const entries = cache.entries[canonicalUrl] || [];
|
|
745
|
-
|
|
784
|
+
|
|
746
785
|
if (entries.length === 0) {
|
|
747
786
|
throw new Error(`Cannot resolve canonical URL: ${canonicalUrl}`);
|
|
748
787
|
}
|
|
749
788
|
|
|
750
789
|
let filtered = [...entries];
|
|
751
|
-
|
|
790
|
+
|
|
752
791
|
if (options?.package) {
|
|
753
|
-
filtered = filtered.filter(e => e.package?.name === options.package);
|
|
792
|
+
filtered = filtered.filter((e) => e.package?.name === options.package);
|
|
754
793
|
}
|
|
755
|
-
|
|
794
|
+
|
|
756
795
|
if (options?.version) {
|
|
757
|
-
filtered = filtered.filter(e => e.version === options.version);
|
|
796
|
+
filtered = filtered.filter((e) => e.version === options.version);
|
|
758
797
|
}
|
|
759
798
|
|
|
760
799
|
if (filtered.length === 0) {
|
|
761
|
-
throw new Error(
|
|
800
|
+
throw new Error(
|
|
801
|
+
`No matching resource found for ${canonicalUrl} with given options`,
|
|
802
|
+
);
|
|
762
803
|
}
|
|
763
804
|
|
|
764
805
|
return filtered[0]!;
|
|
@@ -770,7 +811,7 @@ export const CanonicalManager = (config: Config): CanonicalManager => {
|
|
|
770
811
|
package?: string;
|
|
771
812
|
version?: string;
|
|
772
813
|
sourceContext?: SourceContext;
|
|
773
|
-
}
|
|
814
|
+
},
|
|
774
815
|
): Promise<Resource> => {
|
|
775
816
|
const entry = await resolveEntry(canonicalUrl, options);
|
|
776
817
|
return read(entry);
|
|
@@ -778,20 +819,20 @@ export const CanonicalManager = (config: Config): CanonicalManager => {
|
|
|
778
819
|
|
|
779
820
|
const read = async (reference: Reference): Promise<Resource> => {
|
|
780
821
|
ensureInitialized();
|
|
781
|
-
|
|
822
|
+
|
|
782
823
|
const metadata = cache.referenceManager.get(reference.id);
|
|
783
824
|
if (!metadata) {
|
|
784
825
|
throw new Error(`Invalid reference ID: ${reference.id}`);
|
|
785
826
|
}
|
|
786
827
|
|
|
787
828
|
try {
|
|
788
|
-
const content = await fs.readFile(metadata.filePath,
|
|
829
|
+
const content = await fs.readFile(metadata.filePath, "utf-8");
|
|
789
830
|
const resource = JSON.parse(content);
|
|
790
|
-
|
|
831
|
+
|
|
791
832
|
return {
|
|
792
833
|
...resource,
|
|
793
834
|
id: reference.id,
|
|
794
|
-
resourceType: reference.resourceType
|
|
835
|
+
resourceType: reference.resourceType,
|
|
795
836
|
};
|
|
796
837
|
} catch (err) {
|
|
797
838
|
throw new Error(`Failed to read resource: ${err}`);
|
|
@@ -806,9 +847,9 @@ export const CanonicalManager = (config: Config): CanonicalManager => {
|
|
|
806
847
|
package?: PackageId;
|
|
807
848
|
}): Promise<IndexEntry[]> => {
|
|
808
849
|
ensureInitialized();
|
|
809
|
-
|
|
850
|
+
|
|
810
851
|
let results: IndexEntry[] = [];
|
|
811
|
-
|
|
852
|
+
|
|
812
853
|
if (params.url) {
|
|
813
854
|
results = cache.entries[params.url] || [];
|
|
814
855
|
} else {
|
|
@@ -816,27 +857,27 @@ export const CanonicalManager = (config: Config): CanonicalManager => {
|
|
|
816
857
|
results.push(...entries);
|
|
817
858
|
}
|
|
818
859
|
}
|
|
819
|
-
|
|
860
|
+
|
|
820
861
|
if (params.kind !== undefined) {
|
|
821
|
-
results = results.filter(e => e.kind === params.kind);
|
|
862
|
+
results = results.filter((e) => e.kind === params.kind);
|
|
822
863
|
}
|
|
823
|
-
|
|
864
|
+
|
|
824
865
|
if (params.type !== undefined) {
|
|
825
|
-
results = results.filter(e => e.type === params.type);
|
|
866
|
+
results = results.filter((e) => e.type === params.type);
|
|
826
867
|
}
|
|
827
|
-
|
|
868
|
+
|
|
828
869
|
if (params.version !== undefined) {
|
|
829
|
-
results = results.filter(e => e.version === params.version);
|
|
870
|
+
results = results.filter((e) => e.version === params.version);
|
|
830
871
|
}
|
|
831
|
-
|
|
872
|
+
|
|
832
873
|
if (params.package) {
|
|
833
874
|
const pkg = params.package;
|
|
834
|
-
results = results.filter(
|
|
835
|
-
e
|
|
836
|
-
|
|
875
|
+
results = results.filter(
|
|
876
|
+
(e) =>
|
|
877
|
+
e.package?.name === pkg.name && e.package?.version === pkg.version,
|
|
837
878
|
);
|
|
838
879
|
}
|
|
839
|
-
|
|
880
|
+
|
|
840
881
|
return results;
|
|
841
882
|
};
|
|
842
883
|
|
|
@@ -848,10 +889,98 @@ export const CanonicalManager = (config: Config): CanonicalManager => {
|
|
|
848
889
|
package?: PackageId;
|
|
849
890
|
}): Promise<Resource[]> => {
|
|
850
891
|
const entries = await searchEntries(params);
|
|
851
|
-
const resources = await Promise.all(entries.map(entry => read(entry)));
|
|
892
|
+
const resources = await Promise.all(entries.map((entry) => read(entry)));
|
|
852
893
|
return resources;
|
|
853
894
|
};
|
|
854
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
|
+
|
|
855
984
|
return {
|
|
856
985
|
init,
|
|
857
986
|
destroy,
|
|
@@ -860,9 +989,10 @@ export const CanonicalManager = (config: Config): CanonicalManager => {
|
|
|
860
989
|
resolve,
|
|
861
990
|
read,
|
|
862
991
|
searchEntries,
|
|
863
|
-
search
|
|
992
|
+
search,
|
|
993
|
+
smartSearch,
|
|
864
994
|
};
|
|
865
995
|
};
|
|
866
996
|
|
|
867
997
|
// Default export
|
|
868
|
-
export default CanonicalManager;
|
|
998
|
+
export default CanonicalManager;
|