@atomic-ehr/fhir-canonical-manager 0.0.8 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/README.md +72 -0
  2. package/dist/cache/core.d.ts +7 -0
  3. package/dist/cache/core.d.ts.map +1 -0
  4. package/dist/cache/core.js +11 -0
  5. package/dist/cache/core.js.map +1 -0
  6. package/dist/cache/index.d.ts +4 -0
  7. package/dist/cache/index.d.ts.map +1 -0
  8. package/dist/cache/index.js +4 -0
  9. package/dist/cache/index.js.map +1 -0
  10. package/dist/cache/persistence.d.ts +5 -0
  11. package/dist/cache/persistence.d.ts.map +1 -0
  12. package/dist/cache/persistence.js +25 -0
  13. package/dist/cache/persistence.js.map +1 -0
  14. package/dist/cache/validation.d.ts +2 -0
  15. package/dist/cache/validation.d.ts.map +1 -0
  16. package/dist/cache/validation.js +21 -0
  17. package/dist/cache/validation.js.map +1 -0
  18. package/dist/cli/index.d.ts.map +1 -1
  19. package/dist/cli/index.js +8 -4
  20. package/dist/cli/index.js.map +1 -1
  21. package/dist/cli/searchparam.d.ts +2 -0
  22. package/dist/cli/searchparam.d.ts.map +1 -0
  23. package/dist/cli/searchparam.js +131 -0
  24. package/dist/cli/searchparam.js.map +1 -0
  25. package/dist/constants.d.ts +2 -0
  26. package/dist/constants.d.ts.map +1 -0
  27. package/dist/constants.js +2 -0
  28. package/dist/constants.js.map +1 -0
  29. package/dist/fs/index.d.ts +2 -0
  30. package/dist/fs/index.d.ts.map +1 -0
  31. package/dist/fs/index.js +2 -0
  32. package/dist/fs/index.js.map +1 -0
  33. package/dist/fs/utils.d.ts +4 -0
  34. package/dist/fs/utils.d.ts.map +1 -0
  35. package/dist/fs/utils.js +23 -0
  36. package/dist/fs/utils.js.map +1 -0
  37. package/dist/index.d.ts +6 -111
  38. package/dist/index.d.ts.map +1 -1
  39. package/dist/index.js +5 -592
  40. package/dist/index.js.map +1 -1
  41. package/dist/manager/canonical.d.ts +3 -0
  42. package/dist/manager/canonical.d.ts.map +1 -0
  43. package/dist/manager/canonical.js +194 -0
  44. package/dist/manager/canonical.js.map +1 -0
  45. package/dist/manager/index.d.ts +3 -0
  46. package/dist/manager/index.d.ts.map +1 -0
  47. package/dist/manager/index.js +3 -0
  48. package/dist/manager/index.js.map +1 -0
  49. package/dist/package/detector.d.ts +2 -0
  50. package/dist/package/detector.d.ts.map +1 -0
  51. package/dist/package/detector.js +19 -0
  52. package/dist/package/detector.js.map +1 -0
  53. package/dist/package/index.d.ts +3 -0
  54. package/dist/package/index.d.ts.map +1 -0
  55. package/dist/package/index.js +3 -0
  56. package/dist/package/index.js.map +1 -0
  57. package/dist/package/installer.d.ts +2 -0
  58. package/dist/package/installer.d.ts.map +1 -0
  59. package/dist/package/installer.js +55 -0
  60. package/dist/package/installer.js.map +1 -0
  61. package/dist/reference/index.d.ts +5 -0
  62. package/dist/reference/index.d.ts.map +1 -0
  63. package/dist/reference/index.js +4 -0
  64. package/dist/reference/index.js.map +1 -0
  65. package/dist/reference/manager.d.ts +10 -0
  66. package/dist/reference/manager.d.ts.map +1 -0
  67. package/dist/reference/manager.js +36 -0
  68. package/dist/reference/manager.js.map +1 -0
  69. package/dist/reference/store.d.ts +6 -0
  70. package/dist/reference/store.d.ts.map +1 -0
  71. package/dist/reference/store.js +6 -0
  72. package/dist/reference/store.js.map +1 -0
  73. package/dist/resolver/context.d.ts +4 -0
  74. package/dist/resolver/context.d.ts.map +1 -0
  75. package/dist/resolver/context.js +14 -0
  76. package/dist/resolver/context.js.map +1 -0
  77. package/dist/resolver/index.d.ts +2 -0
  78. package/dist/resolver/index.d.ts.map +1 -0
  79. package/dist/resolver/index.js +2 -0
  80. package/dist/resolver/index.js.map +1 -0
  81. package/dist/scanner/directory.d.ts +3 -0
  82. package/dist/scanner/directory.d.ts.map +1 -0
  83. package/dist/scanner/directory.js +33 -0
  84. package/dist/scanner/directory.js.map +1 -0
  85. package/dist/scanner/index.d.ts +5 -0
  86. package/dist/scanner/index.d.ts.map +1 -0
  87. package/dist/scanner/index.js +5 -0
  88. package/dist/scanner/index.js.map +1 -0
  89. package/dist/scanner/package.d.ts +3 -0
  90. package/dist/scanner/package.d.ts.map +1 -0
  91. package/dist/scanner/package.js +26 -0
  92. package/dist/scanner/package.js.map +1 -0
  93. package/dist/scanner/parser.d.ts +5 -0
  94. package/dist/scanner/parser.d.ts.map +1 -0
  95. package/dist/scanner/parser.js +39 -0
  96. package/dist/scanner/parser.js.map +1 -0
  97. package/dist/scanner/processor.d.ts +4 -0
  98. package/dist/scanner/processor.d.ts.map +1 -0
  99. package/dist/scanner/processor.js +53 -0
  100. package/dist/scanner/processor.js.map +1 -0
  101. package/dist/search/index.d.ts +3 -0
  102. package/dist/search/index.d.ts.map +1 -0
  103. package/dist/search/index.js +3 -0
  104. package/dist/search/index.js.map +1 -0
  105. package/dist/search/smart.d.ts +9 -0
  106. package/dist/search/smart.d.ts.map +1 -0
  107. package/dist/search/smart.js +31 -0
  108. package/dist/search/smart.js.map +1 -0
  109. package/dist/search/terms.d.ts +2 -0
  110. package/dist/search/terms.d.ts.map +1 -0
  111. package/dist/search/terms.js +19 -0
  112. package/dist/search/terms.js.map +1 -0
  113. package/dist/types/core.d.ts +91 -0
  114. package/dist/types/core.d.ts.map +1 -0
  115. package/dist/types/core.js +2 -0
  116. package/dist/types/core.js.map +1 -0
  117. package/dist/types/index.d.ts +3 -0
  118. package/dist/types/index.d.ts.map +1 -0
  119. package/dist/types/index.js +3 -0
  120. package/dist/types/index.js.map +1 -0
  121. package/dist/types/internal.d.ts +48 -0
  122. package/dist/types/internal.d.ts.map +1 -0
  123. package/dist/types/internal.js +2 -0
  124. package/dist/types/internal.js.map +1 -0
  125. package/package.json +1 -1
  126. package/src/cache/core.ts +20 -0
  127. package/src/cache/index.ts +7 -0
  128. package/src/cache/persistence.ts +39 -0
  129. package/src/cache/validation.ts +27 -0
  130. package/src/cli/index.ts +8 -4
  131. package/src/cli/searchparam.ts +162 -0
  132. package/src/constants.ts +6 -0
  133. package/src/fs/index.ts +5 -0
  134. package/src/fs/utils.ts +28 -0
  135. package/src/index.ts +13 -990
  136. package/src/manager/canonical.ts +338 -0
  137. package/src/manager/index.ts +8 -0
  138. package/src/package/detector.ts +24 -0
  139. package/src/package/index.ts +6 -0
  140. package/src/package/installer.ts +77 -0
  141. package/src/reference/index.ts +10 -0
  142. package/src/reference/manager.ts +51 -0
  143. package/src/reference/store.ts +15 -0
  144. package/src/resolver/context.ts +25 -0
  145. package/src/resolver/index.ts +5 -0
  146. package/src/scanner/directory.ts +42 -0
  147. package/src/scanner/index.ts +8 -0
  148. package/src/scanner/package.ts +38 -0
  149. package/src/scanner/parser.ts +42 -0
  150. package/src/scanner/processor.ts +69 -0
  151. package/src/search/index.ts +6 -0
  152. package/src/search/smart.ts +57 -0
  153. package/src/search/terms.ts +22 -0
  154. package/src/types/core.ts +117 -0
  155. package/src/types/index.ts +6 -0
  156. package/src/types/internal.ts +57 -0
@@ -0,0 +1,338 @@
1
+ /**
2
+ * Main CanonicalManager implementation
3
+ */
4
+
5
+ import * as path from 'path';
6
+ import * as fs from 'fs/promises';
7
+ import type {
8
+ Config,
9
+ CanonicalManager,
10
+ PackageId,
11
+ IndexEntry,
12
+ Resource,
13
+ Reference,
14
+ SourceContext,
15
+ SearchParameter,
16
+ } from '../types';
17
+ import { DEFAULT_REGISTRY } from '../constants';
18
+ import { ensureDir } from '../fs';
19
+ import { installPackages } from '../package';
20
+ import {
21
+ createCache,
22
+ saveCacheToDisk,
23
+ loadCacheFromDisk,
24
+ computePackageLockHash,
25
+ type ExtendedCache,
26
+ } from '../cache';
27
+ import { scanDirectory } from '../scanner';
28
+ import { resolveWithContext } from '../resolver';
29
+ import { filterBySmartSearch } from '../search';
30
+
31
+ export const createCanonicalManager = (config: Config): CanonicalManager => {
32
+ const { packages, workingDir } = config;
33
+ // Ensure registry URL ends with /
34
+ const registry = config.registry
35
+ ? config.registry.endsWith("/")
36
+ ? config.registry
37
+ : `${config.registry}/`
38
+ : DEFAULT_REGISTRY;
39
+ const nodeModulesPath = path.join(workingDir, "node_modules");
40
+ const cacheDir = path.join(workingDir, ".fcm", "cache");
41
+
42
+ let cache = createCache();
43
+ let initialized = false;
44
+ const searchParamsCache = new Map<string, SearchParameter[]>();
45
+
46
+ const ensureInitialized = (): void => {
47
+ if (!initialized) {
48
+ throw new Error("CanonicalManager not initialized. Call init() first.");
49
+ }
50
+ };
51
+
52
+ const init = async (): Promise<void> => {
53
+ if (initialized) return;
54
+
55
+ // Ensure directories exist
56
+ await ensureDir(workingDir);
57
+ await ensureDir(cacheDir);
58
+
59
+ // Get current package-lock.json hash
60
+ const currentPackageLockHash = await computePackageLockHash(workingDir);
61
+
62
+ // Try to load cache from disk
63
+ const cachedData = await loadCacheFromDisk(cacheDir);
64
+
65
+ // Check if cache is valid (exists and package-lock.json hasn't changed)
66
+ const cacheValid =
67
+ cachedData &&
68
+ cachedData.packageLockHash === currentPackageLockHash &&
69
+ currentPackageLockHash !== null;
70
+
71
+ if (cacheValid) {
72
+ // Restore cache from disk
73
+ cache.entries = cachedData.entries;
74
+ cache.packages = cachedData.packages;
75
+ Object.entries(cachedData.references).forEach(([id, metadata]) => {
76
+ cache.referenceManager.set(id, metadata);
77
+ });
78
+ } else {
79
+ // Cache is invalid or doesn't exist - rebuild it
80
+ if (cachedData && cachedData.packageLockHash !== currentPackageLockHash) {
81
+ console.log("Package dependencies have changed, rebuilding index...");
82
+ }
83
+
84
+ // Install packages if needed
85
+ await installPackages(packages, workingDir, registry);
86
+
87
+ // Clear cache before scanning
88
+ cache = createCache();
89
+
90
+ // Scan installed packages
91
+ await scanDirectory(nodeModulesPath, cache);
92
+
93
+ // Save cache to disk with current package-lock hash
94
+ await saveCacheToDisk(cache, cacheDir, workingDir);
95
+ }
96
+
97
+ initialized = true;
98
+ };
99
+
100
+ const destroy = async (): Promise<void> => {
101
+ cache.entries = {};
102
+ cache.packages = {};
103
+ cache.referenceManager.clear();
104
+ searchParamsCache.clear();
105
+ initialized = false;
106
+ };
107
+
108
+ const getPackages = async (): Promise<PackageId[]> => {
109
+ ensureInitialized();
110
+ return Object.values(cache.packages).map((p) => p.id);
111
+ };
112
+
113
+ const resolveEntry = async (
114
+ canonicalUrl: string,
115
+ options?: {
116
+ package?: string;
117
+ version?: string;
118
+ sourceContext?: SourceContext;
119
+ },
120
+ ): Promise<IndexEntry> => {
121
+ ensureInitialized();
122
+
123
+ if (options?.sourceContext) {
124
+ const contextResolved = await resolveWithContext(
125
+ canonicalUrl,
126
+ options.sourceContext,
127
+ cache,
128
+ resolveEntry,
129
+ );
130
+ if (contextResolved) {
131
+ return contextResolved;
132
+ }
133
+ }
134
+
135
+ const entries = cache.entries[canonicalUrl] || [];
136
+
137
+ if (entries.length === 0) {
138
+ throw new Error(`Cannot resolve canonical URL: ${canonicalUrl}`);
139
+ }
140
+
141
+ let filtered = [...entries];
142
+
143
+ if (options?.package) {
144
+ filtered = filtered.filter((e) => e.package?.name === options.package);
145
+ }
146
+
147
+ if (options?.version) {
148
+ filtered = filtered.filter((e) => e.version === options.version);
149
+ }
150
+
151
+ if (filtered.length === 0) {
152
+ throw new Error(
153
+ `No matching resource found for ${canonicalUrl} with given options`,
154
+ );
155
+ }
156
+
157
+ return filtered[0]!;
158
+ };
159
+
160
+ const resolve = async (
161
+ canonicalUrl: string,
162
+ options?: {
163
+ package?: string;
164
+ version?: string;
165
+ sourceContext?: SourceContext;
166
+ },
167
+ ): Promise<Resource> => {
168
+ const entry = await resolveEntry(canonicalUrl, options);
169
+ return read(entry);
170
+ };
171
+
172
+ const read = async (reference: Reference): Promise<Resource> => {
173
+ ensureInitialized();
174
+
175
+ const metadata = cache.referenceManager.get(reference.id);
176
+ if (!metadata) {
177
+ throw new Error(`Invalid reference ID: ${reference.id}`);
178
+ }
179
+
180
+ try {
181
+ const content = await fs.readFile(metadata.filePath, "utf-8");
182
+ const resource = JSON.parse(content);
183
+
184
+ return {
185
+ ...resource,
186
+ id: reference.id,
187
+ resourceType: reference.resourceType,
188
+ };
189
+ } catch (err) {
190
+ throw new Error(`Failed to read resource: ${err}`);
191
+ }
192
+ };
193
+
194
+ const searchEntries = async (params: {
195
+ kind?: string;
196
+ url?: string;
197
+ type?: string;
198
+ version?: string;
199
+ package?: PackageId;
200
+ }): Promise<IndexEntry[]> => {
201
+ ensureInitialized();
202
+
203
+ let results: IndexEntry[] = [];
204
+
205
+ if (params.url) {
206
+ results = cache.entries[params.url] || [];
207
+ } else {
208
+ for (const entries of Object.values(cache.entries)) {
209
+ results.push(...entries);
210
+ }
211
+ }
212
+
213
+ if (params.kind !== undefined) {
214
+ results = results.filter((e) => e.kind === params.kind);
215
+ }
216
+
217
+ if (params.type !== undefined) {
218
+ results = results.filter((e) => e.type === params.type);
219
+ }
220
+
221
+ if (params.version !== undefined) {
222
+ results = results.filter((e) => e.version === params.version);
223
+ }
224
+
225
+ if (params.package) {
226
+ const pkg = params.package;
227
+ results = results.filter(
228
+ (e) =>
229
+ e.package?.name === pkg.name && e.package?.version === pkg.version,
230
+ );
231
+ }
232
+
233
+ return results;
234
+ };
235
+
236
+ const search = async (params: {
237
+ kind?: string;
238
+ url?: string;
239
+ type?: string;
240
+ version?: string;
241
+ package?: PackageId;
242
+ }): Promise<Resource[]> => {
243
+ const entries = await searchEntries(params);
244
+ const resources = await Promise.all(entries.map((entry) => read(entry)));
245
+ return resources;
246
+ };
247
+
248
+ const smartSearch = async (
249
+ searchTerms: string[],
250
+ filters?: {
251
+ resourceType?: string;
252
+ type?: string;
253
+ kind?: string;
254
+ package?: PackageId;
255
+ },
256
+ ): Promise<IndexEntry[]> => {
257
+ ensureInitialized();
258
+
259
+ // Start with base search using filters
260
+ let results = await searchEntries({
261
+ kind: filters?.kind,
262
+ package: filters?.package,
263
+ });
264
+
265
+ // Apply resourceType filter
266
+ if (filters?.resourceType) {
267
+ results = results.filter(
268
+ (entry) => entry.resourceType === filters.resourceType,
269
+ );
270
+ }
271
+
272
+ // Apply type filter
273
+ if (filters?.type) {
274
+ results = results.filter((entry) => entry.type === filters.type);
275
+ }
276
+
277
+ // Apply smart search filtering
278
+ return filterBySmartSearch(results, searchTerms);
279
+ };
280
+
281
+ const getSearchParametersForResource = async (
282
+ resourceType: string
283
+ ): Promise<SearchParameter[]> => {
284
+ ensureInitialized();
285
+
286
+ // Check cache first
287
+ if (searchParamsCache.has(resourceType)) {
288
+ return searchParamsCache.get(resourceType)!;
289
+ }
290
+
291
+ // Query all SearchParameter resources
292
+ // Note: We search by resourceType, not type, because for SearchParameter resources,
293
+ // 'type' refers to the search parameter type (token, string, etc.)
294
+ const allEntries = await searchEntries({});
295
+ const searchParamEntries = allEntries.filter(
296
+ entry => entry.resourceType === 'SearchParameter'
297
+ );
298
+
299
+ const results: SearchParameter[] = [];
300
+
301
+ for (const entry of searchParamEntries) {
302
+ const resource = await read(entry);
303
+
304
+ // Check if this parameter applies to the requested resource
305
+ const bases = resource.base || [];
306
+ if (Array.isArray(bases) && bases.includes(resourceType)) {
307
+ // Return the full original resource - it already contains all fields
308
+ // Cast through unknown to satisfy TypeScript
309
+ results.push(resource as unknown as SearchParameter);
310
+ }
311
+ }
312
+
313
+ // Sort by code for consistent output
314
+ results.sort((a, b) => {
315
+ const codeA = a.code || '';
316
+ const codeB = b.code || '';
317
+ return codeA.localeCompare(codeB);
318
+ });
319
+
320
+ // Cache the results
321
+ searchParamsCache.set(resourceType, results);
322
+
323
+ return results;
324
+ };
325
+
326
+ return {
327
+ init,
328
+ destroy,
329
+ packages: getPackages,
330
+ resolveEntry,
331
+ resolve,
332
+ read,
333
+ searchEntries,
334
+ search,
335
+ smartSearch,
336
+ getSearchParametersForResource,
337
+ };
338
+ };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Manager module exports
3
+ */
4
+
5
+ export { createCanonicalManager } from './canonical';
6
+
7
+ // For backward compatibility
8
+ export { createCanonicalManager as CanonicalManager } from './canonical';
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Package manager detection
3
+ */
4
+
5
+ import { exec } from 'child_process';
6
+ import { promisify } from 'util';
7
+
8
+ const execAsync = promisify(exec);
9
+
10
+ export async function detectPackageManager(): Promise<"bun" | "npm" | null> {
11
+ try {
12
+ // Check for bun first
13
+ await execAsync("bun --version");
14
+ return "bun";
15
+ } catch {
16
+ try {
17
+ // Fall back to npm
18
+ await execAsync("npm --version");
19
+ return "npm";
20
+ } catch {
21
+ return null;
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Package management exports
3
+ */
4
+
5
+ export { detectPackageManager } from './detector';
6
+ export { installPackages } from './installer';
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Package installation functionality
3
+ */
4
+
5
+ import * as fs from 'fs/promises';
6
+ import * as path from 'path';
7
+ import { exec } from 'child_process';
8
+ import { promisify } from 'util';
9
+ import { ensureDir, fileExists } from '../fs';
10
+ import { detectPackageManager } from './detector';
11
+
12
+ const execAsync = promisify(exec);
13
+
14
+ export const installPackages = async (
15
+ packages: string[],
16
+ workingDir: string,
17
+ registry?: string,
18
+ ): Promise<void> => {
19
+ await ensureDir(workingDir);
20
+
21
+ // Check if package.json exists
22
+ const packageJsonPath = path.join(workingDir, "package.json");
23
+ if (!(await fileExists(packageJsonPath))) {
24
+ // Create minimal package.json
25
+ const minimalPackageJson = {
26
+ name: "fhir-canonical-manager-workspace",
27
+ version: "1.0.0",
28
+ private: true,
29
+ dependencies: {},
30
+ };
31
+ await fs.writeFile(
32
+ packageJsonPath,
33
+ JSON.stringify(minimalPackageJson, null, 2),
34
+ );
35
+ }
36
+
37
+ // Detect available package manager
38
+ const packageManager = await detectPackageManager();
39
+ if (!packageManager) {
40
+ throw new Error("No package manager found. Please install npm or bun.");
41
+ }
42
+
43
+ // Install packages
44
+ for (const pkg of packages) {
45
+ try {
46
+ if (packageManager === "bun") {
47
+ // Use bun with auth bypass trick for FHIR registry
48
+ const env = {
49
+ ...process.env,
50
+ HOME: workingDir, // Prevent reading user's .npmrc
51
+ NPM_CONFIG_USERCONFIG: "/dev/null", // Extra safety
52
+ };
53
+
54
+ const cmd = registry
55
+ ? `cd ${workingDir} && bun add ${pkg} --registry ${registry}`
56
+ : `cd ${workingDir} && bun add ${pkg}`;
57
+
58
+ await execAsync(cmd, {
59
+ env,
60
+ maxBuffer: 10 * 1024 * 1024, // 10MB buffer
61
+ });
62
+ } else {
63
+ // Use npm (handles auth correctly)
64
+ const cmd = registry
65
+ ? `cd ${workingDir} && npm add ${pkg} --registry ${registry}`
66
+ : `cd ${workingDir} && npm add ${pkg}`;
67
+
68
+ await execAsync(cmd, {
69
+ maxBuffer: 10 * 1024 * 1024, // 10MB buffer
70
+ });
71
+ }
72
+ } catch (err) {
73
+ console.error(`Failed to install package ${pkg}:`, err);
74
+ throw err;
75
+ }
76
+ }
77
+ };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Reference management exports
3
+ */
4
+
5
+ export { generateReferenceId } from './store';
6
+ export { createReferenceManager } from './manager';
7
+ export type { ReferenceManager } from './manager';
8
+
9
+ // For backward compatibility - function alias (will be deprecated)
10
+ export { createReferenceManager as ReferenceManagerFactory } from './manager';
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Reference manager factory
3
+ */
4
+
5
+ import type { Reference, ReferenceMetadata, ReferenceStore } from '../types';
6
+ import { generateReferenceId } from './store';
7
+
8
+ export interface ReferenceManager extends ReferenceStore {
9
+ generateId: typeof generateReferenceId;
10
+ getIdsByUrl: (url: string) => string[];
11
+ createReference: (id: string, metadata: ReferenceMetadata) => Reference;
12
+ getAllReferences: () => Record<string, ReferenceMetadata>;
13
+ }
14
+
15
+ export const createReferenceManager = (): ReferenceManager => {
16
+ const references: Record<string, ReferenceMetadata> = {};
17
+ const urlToIds: Record<string, string[]> = {};
18
+
19
+ const set = (id: string, metadata: ReferenceMetadata): void => {
20
+ references[id] = metadata;
21
+ if (metadata.url) {
22
+ if (!urlToIds[metadata.url]) {
23
+ urlToIds[metadata.url] = [];
24
+ }
25
+ const ids = urlToIds[metadata.url];
26
+ if (ids && !ids.includes(id)) {
27
+ ids.push(id);
28
+ }
29
+ }
30
+ };
31
+
32
+ const clear = (): void => {
33
+ Object.keys(references).forEach((key) => delete references[key]);
34
+ Object.keys(urlToIds).forEach((key) => delete urlToIds[key]);
35
+ };
36
+
37
+ return {
38
+ generateId: generateReferenceId,
39
+ get: (id: string) => references[id],
40
+ set,
41
+ has: (id: string) => id in references,
42
+ clear,
43
+ size: () => Object.keys(references).length,
44
+ getIdsByUrl: (url: string) => urlToIds[url] || [],
45
+ createReference: (id: string, metadata: ReferenceMetadata): Reference => ({
46
+ id,
47
+ resourceType: metadata.resourceType,
48
+ }),
49
+ getAllReferences: () => references,
50
+ };
51
+ };
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Reference store implementation
3
+ */
4
+
5
+ import { createHash } from 'crypto';
6
+ import type { ReferenceMetadata, ReferenceStore } from '../types';
7
+
8
+ export const generateReferenceId = (metadata: {
9
+ packageName: string;
10
+ packageVersion: string;
11
+ filePath: string;
12
+ }): string => {
13
+ const input = `${metadata.packageName}@${metadata.packageVersion}:${metadata.filePath}`;
14
+ return createHash("sha256").update(input).digest("base64url");
15
+ };
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Context-aware resolution functionality
3
+ */
4
+
5
+ import type { SourceContext, IndexEntry } from '../types';
6
+ import type { ExtendedCache } from '../cache/core';
7
+
8
+ export const resolveWithContext = async (
9
+ url: string,
10
+ context: SourceContext,
11
+ cache: ExtendedCache,
12
+ resolveEntry: (url: string, options?: any) => Promise<IndexEntry>,
13
+ ): Promise<IndexEntry | null> => {
14
+ if (context.package) {
15
+ try {
16
+ return await resolveEntry(url, {
17
+ package: context.package.name,
18
+ version: context.package.version,
19
+ });
20
+ } catch {
21
+ // Fall through to global resolution
22
+ }
23
+ }
24
+ return null;
25
+ };
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Resolver module exports
3
+ */
4
+
5
+ export { resolveWithContext } from './context';
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Directory scanning functionality
3
+ */
4
+
5
+ import * as fs from 'fs/promises';
6
+ import * as path from 'path';
7
+ import type { ExtendedCache } from '../cache/core';
8
+ import { isFhirPackage } from '../fs';
9
+ import { scanPackage } from './package';
10
+
11
+ export const scanDirectory = async (
12
+ dirPath: string,
13
+ cache: ExtendedCache,
14
+ ): Promise<void> => {
15
+ try {
16
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
17
+
18
+ for (const entry of entries) {
19
+ if (!entry.isDirectory()) continue;
20
+
21
+ const fullPath = path.join(dirPath, entry.name);
22
+
23
+ if (entry.name.startsWith("@")) {
24
+ const scopedEntries = await fs.readdir(fullPath, {
25
+ withFileTypes: true,
26
+ });
27
+ for (const scopedEntry of scopedEntries) {
28
+ if (!scopedEntry.isDirectory()) continue;
29
+
30
+ const scopedPath = path.join(fullPath, scopedEntry.name);
31
+ if (await isFhirPackage(scopedPath)) {
32
+ await scanPackage(scopedPath, cache);
33
+ }
34
+ }
35
+ } else if (await isFhirPackage(fullPath)) {
36
+ await scanPackage(fullPath, cache);
37
+ }
38
+ }
39
+ } catch {
40
+ // Silently ignore scan errors
41
+ }
42
+ };
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Scanner module exports
3
+ */
4
+
5
+ export { parseIndex, isValidFileEntry, isValidIndexFile } from './parser';
6
+ export { processIndex } from './processor';
7
+ export { scanPackage } from './package';
8
+ export { scanDirectory } from './directory';
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Package scanning functionality
3
+ */
4
+
5
+ import * as fs from 'fs/promises';
6
+ import * as path from 'path';
7
+ import type { PackageJson, PackageInfo } from '../types';
8
+ import type { ExtendedCache } from '../cache/core';
9
+ import { fileExists } from '../fs';
10
+ import { processIndex } from './processor';
11
+
12
+ export const scanPackage = async (
13
+ packagePath: string,
14
+ cache: ExtendedCache,
15
+ ): Promise<void> => {
16
+ try {
17
+ const packageJsonPath = path.join(packagePath, "package.json");
18
+ const packageJsonContent = await fs.readFile(packageJsonPath, "utf-8");
19
+ const packageJson: PackageJson = JSON.parse(packageJsonContent);
20
+
21
+ const packageInfo: PackageInfo = {
22
+ id: { name: packageJson.name, version: packageJson.version },
23
+ path: packagePath,
24
+ canonical: packageJson.canonical,
25
+ fhirVersions: packageJson.fhirVersions,
26
+ };
27
+ cache.packages[packageJson.name] = packageInfo;
28
+
29
+ await processIndex(packagePath, packageJson, cache);
30
+
31
+ const examplesPath = path.join(packagePath, "examples");
32
+ if (await fileExists(path.join(examplesPath, ".index.json"))) {
33
+ await processIndex(examplesPath, packageJson, cache);
34
+ }
35
+ } catch {
36
+ // Silently ignore package scan errors
37
+ }
38
+ };
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Index file parsing and validation
3
+ */
4
+
5
+ import type { IndexFile, IndexFileEntry } from '../types';
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")
11
+ return false;
12
+ if (!entry.id || typeof entry.id !== "string") return false;
13
+
14
+ const optionalStringFields = ["url", "version", "kind", "type"];
15
+ for (const field of optionalStringFields) {
16
+ if (entry[field] !== undefined && typeof entry[field] !== "string") {
17
+ return false;
18
+ }
19
+ }
20
+
21
+ return true;
22
+ };
23
+
24
+ export const isValidIndexFile = (data: any): boolean => {
25
+ if (!data || typeof data !== "object") return false;
26
+ if (!data["index-version"] || typeof data["index-version"] !== "number")
27
+ return false;
28
+ if (!Array.isArray(data.files)) return false;
29
+ return data.files.every((file: any) => isValidFileEntry(file));
30
+ };
31
+
32
+ export const parseIndex = (content: string, filePath: string): IndexFile | null => {
33
+ try {
34
+ const data = JSON.parse(content);
35
+ if (!isValidIndexFile(data)) {
36
+ return null;
37
+ }
38
+ return data as IndexFile;
39
+ } catch {
40
+ return null;
41
+ }
42
+ };