babelfhir-ts 1.2.2 → 1.2.4

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.
@@ -1,4 +1,4 @@
1
- import { generateIntoPackage, resetFetchFailureTracking, getFetchFailureWarning } from "../generator/index.js";
1
+ import { generateIntoPackageDirect, resetFetchFailureTracking, getFetchFailureWarning } from "../generator/index.js";
2
2
  import { getCacheConfig, clearAllCaches } from "../generator/core/cacheConfig.js";
3
3
  import { buildQualityReport, formatReportSummary, resetDiagnostics } from "../generator/core/sdDiagnostics.js";
4
4
  import fs from 'fs';
@@ -22,18 +22,29 @@ function cleanupCache() {
22
22
  console.log('Cache cleaned.');
23
23
  }
24
24
  }
25
- /** Detect the package manager used in the current working directory */
26
- function detectPackageManager() {
27
- const cwd = process.cwd();
25
+ /** Detect the package manager by walking up from startDir (default: cwd) to find a lock file */
26
+ export function detectPackageManager(startDir) {
28
27
  const isWin = process.platform === 'win32';
29
- if (fs.existsSync(path.join(cwd, 'bun.lock')) || fs.existsSync(path.join(cwd, 'bun.lockb'))) {
30
- return { cmd: isWin ? 'bun.exe' : 'bun', args: ['add'] };
31
- }
32
- if (fs.existsSync(path.join(cwd, 'pnpm-lock.yaml'))) {
33
- return { cmd: isWin ? 'pnpm.cmd' : 'pnpm', args: ['add'] };
34
- }
35
- if (fs.existsSync(path.join(cwd, 'yarn.lock'))) {
36
- return { cmd: isWin ? 'yarn.cmd' : 'yarn', args: ['add'] };
28
+ // Walk up the directory tree — in monorepos the lock file lives at the root,
29
+ // not in the package subdirectory where the command is run.
30
+ let dir = startDir ?? process.cwd();
31
+ while (true) {
32
+ if (fs.existsSync(path.join(dir, 'bun.lock')) || fs.existsSync(path.join(dir, 'bun.lockb'))) {
33
+ return { cmd: isWin ? 'bun.exe' : 'bun', args: ['add'] };
34
+ }
35
+ if (fs.existsSync(path.join(dir, 'pnpm-lock.yaml'))) {
36
+ return { cmd: isWin ? 'pnpm.cmd' : 'pnpm', args: ['add'] };
37
+ }
38
+ if (fs.existsSync(path.join(dir, 'yarn.lock'))) {
39
+ return { cmd: isWin ? 'yarn.cmd' : 'yarn', args: ['add'] };
40
+ }
41
+ if (fs.existsSync(path.join(dir, 'package-lock.json'))) {
42
+ return { cmd: isWin ? 'npm.cmd' : 'npm', args: ['install'] };
43
+ }
44
+ const parent = path.dirname(dir);
45
+ if (parent === dir)
46
+ break; // reached filesystem root
47
+ dir = parent;
37
48
  }
38
49
  return { cmd: isWin ? 'npm.cmd' : 'npm', args: ['install'] };
39
50
  }
@@ -94,10 +105,10 @@ function npmInstall(packagePath) {
94
105
  }
95
106
  export async function handleInstallCommand(opts) {
96
107
  const { packageToInstall, registry, generationFlags, downloadPackage } = opts;
97
- // Default to cleaning cache for install command
98
- if (!generationFlags.noCache) {
99
- generationFlags.noCache = true;
100
- }
108
+ // Keep cache by default dependency packages are large (hl7.fhir.r4.core ~50 MB)
109
+ // and re-downloading them every run is the single biggest bottleneck.
110
+ // Users can opt in to cache cleanup with --no-cache.
111
+ let generationCleanup;
101
112
  try {
102
113
  let downloadedPath;
103
114
  if (fs.existsSync(packageToInstall) && (packageToInstall.endsWith('.tgz') || packageToInstall.endsWith('.zip'))) {
@@ -108,11 +119,11 @@ export async function handleInstallCommand(opts) {
108
119
  downloadedPath = await downloadPackage(packageToInstall, registry);
109
120
  }
110
121
  console.log(`Processing package...`);
111
- const baseName = path.basename(downloadedPath, path.extname(downloadedPath));
112
- const outputArchive = path.join(process.cwd(), `${baseName}.with-generated.tgz`);
113
122
  resetFetchFailureTracking();
114
123
  resetDiagnostics();
115
- await generateIntoPackage(downloadedPath, outputArchive, generationFlags);
124
+ // Generate directly into extracted package — no intermediate archive round-trip
125
+ const { generatedDir, cleanup } = await generateIntoPackageDirect(downloadedPath, generationFlags);
126
+ generationCleanup = cleanup;
116
127
  // Clean up temp directory only if we downloaded from registry (not for local files)
117
128
  if (!fs.existsSync(packageToInstall) || !(packageToInstall.endsWith('.tgz') || packageToInstall.endsWith('.zip'))) {
118
129
  const tempDir = path.join(process.cwd(), '.temp-download');
@@ -120,29 +131,11 @@ export async function handleInstallCommand(opts) {
120
131
  fs.rmSync(tempDir, { recursive: true, force: true });
121
132
  }
122
133
  }
123
- console.log(`✓ Package processed successfully: ${path.basename(outputArchive)}`);
124
- // Extract the archive to access generated folder
125
- console.log(`Extracting generated code...`);
126
- const { extract } = await import('tar');
127
- const extractDir = path.join(process.cwd(), '.temp-install');
128
- if (!fs.existsSync(extractDir)) {
129
- fs.mkdirSync(extractDir, { recursive: true });
130
- }
131
- await extract({ cwd: extractDir, file: outputArchive });
132
- // Look for generated folder - could be at root or under 'package' subfolder
133
- let generatedDir = path.join(extractDir, 'generated');
134
- if (!fs.existsSync(generatedDir)) {
135
- generatedDir = path.join(extractDir, 'package', 'generated');
136
- }
137
- if (!fs.existsSync(generatedDir)) {
138
- throw new Error('Generated folder not found in package archive');
139
- }
140
- // Verify package.json exists
134
+ // Verify package.json exists in generated folder
141
135
  const pkgJsonPath = path.join(generatedDir, 'package.json');
142
136
  if (!fs.existsSync(pkgJsonPath)) {
143
137
  throw new Error(`package.json not found in generated folder: ${generatedDir}`);
144
138
  }
145
- // Create a tarball from the generated folder to avoid symlink issues on Windows
146
139
  const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
147
140
  const packageName = pkgJson.name;
148
141
  console.log(`Creating package tarball for ${packageName}...`);
@@ -163,15 +156,9 @@ export async function handleInstallCommand(opts) {
163
156
  }, ['.']);
164
157
  console.log(`Installing ${packageName}...`);
165
158
  await npmInstall(tarballPath);
166
- // Clean up temporary files (but keep the tarball that npm references in package.json under ./lib)
167
- console.log(`Cleaning up temporary files...`);
168
- if (fs.existsSync(extractDir)) {
169
- fs.rmSync(extractDir, { recursive: true, force: true });
170
- }
171
- if (fs.existsSync(outputArchive)) {
172
- fs.unlinkSync(outputArchive);
173
- console.log(`✓ Removed ${path.basename(outputArchive)}`);
174
- }
159
+ // Clean up extracted package tree (but keep the tarball under ./lib)
160
+ generationCleanup();
161
+ generationCleanup = undefined;
175
162
  console.log(`✓ Package tarball saved to ./lib: ${path.basename(tarballPath)}`);
176
163
  showGenerationWarnings();
177
164
  const report = buildQualityReport();
@@ -184,20 +171,12 @@ export async function handleInstallCommand(opts) {
184
171
  console.log(`\n✓ Package ${packageToInstall} installed and ready to use!`);
185
172
  }
186
173
  catch (error) {
187
- // Clean up temporary files on error
188
- for (const dir of ['.temp-download', '.temp-install']) {
189
- const fullPath = path.join(process.cwd(), dir);
190
- if (fs.existsSync(fullPath)) {
191
- fs.rmSync(fullPath, { recursive: true, force: true });
192
- }
193
- }
194
- // Clean up any leftover archive
195
- const baseName = packageToInstall.includes('@')
196
- ? packageToInstall.replace('@', '-').replace(/\//g, '-')
197
- : path.basename(packageToInstall, path.extname(packageToInstall));
198
- const outputArchive = path.join(process.cwd(), `${baseName}.with-generated.tgz`);
199
- if (fs.existsSync(outputArchive)) {
200
- fs.unlinkSync(outputArchive);
174
+ // Clean up extracted package on error
175
+ generationCleanup?.();
176
+ // Clean up temp download directory
177
+ const tempDir = path.join(process.cwd(), '.temp-download');
178
+ if (fs.existsSync(tempDir)) {
179
+ fs.rmSync(tempDir, { recursive: true, force: true });
201
180
  }
202
181
  if (generationFlags.noCache) {
203
182
  cleanupCache();
@@ -44,6 +44,25 @@ export function capitalize(str) {
44
44
  return str;
45
45
  return str.charAt(0).toUpperCase() + str.slice(1);
46
46
  }
47
+ /**
48
+ * Extract base FHIR types from all interface declarations in a file.
49
+ * Handles:
50
+ * - `export interface Foo extends Bar { ... }`
51
+ * - `export interface Foo extends Omit<Bar, 'a' | 'b'> { ... }`
52
+ * Returns ALL candidates — files may have nested types (e.g.,
53
+ * AllergyIntoleranceReaction) before the main resource interface.
54
+ */
55
+ function extractBaseTypes(content) {
56
+ const results = [];
57
+ const allMatches = [...content.matchAll(/export\s+interface\s+\w+\s+extends\s+(?:Omit|Pick|Partial)<(\w+)|export\s+interface\s+\w+\s+extends\s+(\w+)/g)];
58
+ for (const m of allMatches) {
59
+ const candidate = m[1] ?? m[2];
60
+ if (candidate && candidate !== 'Omit' && candidate !== 'Pick' && candidate !== 'Partial') {
61
+ results.push(candidate);
62
+ }
63
+ }
64
+ return results;
65
+ }
47
66
  export function getResourceTypes(options) {
48
67
  const outputDir = options.outputDir;
49
68
  if (!fs.existsSync(outputDir)) {
@@ -51,40 +70,44 @@ export function getResourceTypes(options) {
51
70
  }
52
71
  const files = fs.readdirSync(outputDir);
53
72
  const resourceTypes = new Map(); // Maps profile name -> base resource type
54
- // Check if index.ts exists
55
- const indexPath = path.join(outputDir, "index.ts");
73
+ // Check if index.ts (or index.d.ts after compilation) exists
74
+ const indexPath = fs.existsSync(path.join(outputDir, "index.ts"))
75
+ ? path.join(outputDir, "index.ts")
76
+ : fs.existsSync(path.join(outputDir, "index.d.ts"))
77
+ ? path.join(outputDir, "index.d.ts")
78
+ : null;
56
79
  let exportedTypes = null;
57
- if (fs.existsSync(indexPath)) {
80
+ if (indexPath) {
58
81
  const indexContent = fs.readFileSync(indexPath, "utf-8");
59
82
  exportedTypes = new Set();
60
- // Extract exported type names from index.ts
61
- const exportRegex = /export \* from ['"]\.\/([\w-]+)\.js['"]/g;
83
+ // Extract module names from export statements (both wildcard and named)
84
+ const exportRegex = /export\s+(?:\*|{[^}]+})\s+from\s+['"]\.\/([\w-]+)\.js['"]/g;
62
85
  let match;
63
86
  while ((match = exportRegex.exec(indexContent)) !== null) {
64
87
  exportedTypes.add(match[1]);
65
88
  }
66
89
  }
67
90
  for (const file of files) {
68
- if (file.endsWith("Class.ts")) {
69
- const profileName = file.replace("Class.ts", "");
70
- if (!exportedTypes || exportedTypes.has(profileName)) {
91
+ if (file.endsWith("Class.ts") || file.endsWith("Class.d.ts")) {
92
+ const profileName = file.replace(/Class\.(ts|d\.ts)$/, "");
93
+ if (!exportedTypes || exportedTypes.has(profileName) || exportedTypes.has(`${profileName}Class`)) {
71
94
  const interfaceFile = path.join(outputDir, `${profileName}.ts`);
72
- if (fs.existsSync(interfaceFile)) {
73
- const content = fs.readFileSync(interfaceFile, "utf-8");
95
+ const interfaceDtsFile = path.join(outputDir, `${profileName}.d.ts`);
96
+ const actualFile = fs.existsSync(interfaceFile) ? interfaceFile : fs.existsSync(interfaceDtsFile) ? interfaceDtsFile : null;
97
+ if (actualFile) {
98
+ const content = fs.readFileSync(actualFile, "utf-8");
74
99
  // Check if it has resourceType property (base resource)
75
100
  const resourceTypeMatch = content.match(/resourceType:\s*["'](\w+)["']/);
76
101
  if (resourceTypeMatch) {
77
102
  resourceTypes.set(profileName, resourceTypeMatch[1]);
78
103
  continue;
79
104
  }
80
- // Check if it extends a FHIR resource
81
- const extendsMatch = content.match(/export\s+interface\s+\w+\s+extends\s+(\w+)/);
82
- if (extendsMatch) {
83
- const baseType = extendsMatch[1];
84
- // Check if it's a known FHIR resource type
85
- if (ctx().resourceNames.includes(baseType)) {
86
- resourceTypes.set(profileName, baseType);
87
- }
105
+ // Check if it extends a FHIR resource (handles both `extends Foo` and `extends Omit<Foo, ...>`)
106
+ // Try all interface declarations — files may contain nested types before the main resource.
107
+ const baseTypes = extractBaseTypes(content);
108
+ const knownBase = baseTypes.find(bt => ctx().resourceNames.includes(bt));
109
+ if (knownBase) {
110
+ resourceTypes.set(profileName, knownBase);
88
111
  }
89
112
  }
90
113
  }
@@ -94,19 +117,22 @@ export function getResourceTypes(options) {
94
117
  }
95
118
  export function getBaseResourceType(profileName, outputDir) {
96
119
  const interfaceFile = path.join(outputDir, `${profileName}.ts`);
97
- if (!fs.existsSync(interfaceFile)) {
120
+ const interfaceDtsFile = path.join(outputDir, `${profileName}.d.ts`);
121
+ const actualFile = fs.existsSync(interfaceFile) ? interfaceFile : fs.existsSync(interfaceDtsFile) ? interfaceDtsFile : null;
122
+ if (!actualFile) {
98
123
  return profileName; // fallback
99
124
  }
100
- const content = fs.readFileSync(interfaceFile, "utf-8");
125
+ const content = fs.readFileSync(actualFile, "utf-8");
101
126
  // Check for explicit resourceType property
102
127
  const resourceTypeMatch = content.match(/resourceType:\s*["'](\w+)["']/);
103
128
  if (resourceTypeMatch) {
104
129
  return resourceTypeMatch[1];
105
130
  }
106
- // Check for extends clause
107
- const extendsMatch = content.match(/export\s+interface\s+\w+\s+extends\s+(\w+)/);
108
- if (extendsMatch) {
109
- return extendsMatch[1];
131
+ // Check for extends clause (handles both `extends Foo` and `extends Omit<Foo, ...>`)
132
+ const baseTypes = extractBaseTypes(content);
133
+ if (baseTypes.length > 0) {
134
+ // Prefer a known resource type, fall back to the first candidate
135
+ return baseTypes.find(bt => ctx().resourceNames.includes(bt)) ?? baseTypes[0];
110
136
  }
111
137
  return profileName; // fallback
112
138
  }
@@ -13,7 +13,7 @@
13
13
  import fs from 'fs';
14
14
  import path from 'path';
15
15
  import { getFhirPackagesCacheDir, ensureCacheDir } from '../core/cacheConfig.js';
16
- import { fetchArrayBuffer } from '../core/fetchUtils.js';
16
+ import { downloadFile } from '../core/utils.js';
17
17
  import { logger } from '../../logger.js';
18
18
  const log = logger.withTag('core-resolver');
19
19
  /**
@@ -58,7 +58,7 @@ export async function ensureCorePackage(corePackageSpec) {
58
58
  return candidate;
59
59
  }
60
60
  }
61
- // Not cached — download from registry
61
+ // Not cached — download from registry (streaming to avoid buffering large packages in RAM)
62
62
  ensureCacheDir(configuredCacheDir);
63
63
  const packageDir = path.join(configuredCacheDir, dirName);
64
64
  let downloaded = false;
@@ -67,8 +67,7 @@ export async function ensureCorePackage(corePackageSpec) {
67
67
  const url = `${registry}/${packageName}/${version}`;
68
68
  try {
69
69
  log.info(`Downloading core package ${corePackageSpec} from ${registry}…`);
70
- const response = await fetchArrayBuffer(url);
71
- fs.writeFileSync(tgzPath, Buffer.from(response.data));
70
+ await downloadFile(url, tgzPath);
72
71
  downloaded = true;
73
72
  break;
74
73
  }
@@ -14,7 +14,7 @@
14
14
  import fs from 'fs';
15
15
  import path from 'path';
16
16
  import { getFhirPackagesCacheDir, ensureCacheDir } from '../core/cacheConfig.js';
17
- import { fetchArrayBuffer } from '../core/fetchUtils.js';
17
+ import { downloadFile } from '../core/utils.js';
18
18
  import { logger } from '../../logger.js';
19
19
 
20
20
  const log = logger.withTag('core-resolver');
@@ -88,7 +88,7 @@ export async function ensureCorePackage(corePackageSpec: string): Promise<string
88
88
  }
89
89
  }
90
90
 
91
- // Not cached — download from registry
91
+ // Not cached — download from registry (streaming to avoid buffering large packages in RAM)
92
92
  ensureCacheDir(configuredCacheDir);
93
93
  const packageDir = path.join(configuredCacheDir, dirName);
94
94
 
@@ -98,8 +98,7 @@ export async function ensureCorePackage(corePackageSpec: string): Promise<string
98
98
  const url = `${registry}/${packageName}/${version}`;
99
99
  try {
100
100
  log.info(`Downloading core package ${corePackageSpec} from ${registry}…`);
101
- const response = await fetchArrayBuffer(url);
102
- fs.writeFileSync(tgzPath, Buffer.from(response.data));
101
+ await downloadFile(url, tgzPath);
103
102
  downloaded = true;
104
103
  break;
105
104
  } catch (err) {