babelfhir-ts 1.2.3 → 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.
@@ -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
  }
@@ -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
  }
@@ -321,6 +321,11 @@ export async function generateIntoPackage(packageArchivePath, outArchivePath, fl
321
321
  valueSets = await expandValueSetsWithTx(valueSets, valueSetCodesMap, structureDefinitions, flags.txServer);
322
322
  }
323
323
  const outputDir = path.join(extractedRoot, 'generated');
324
+ // Always start with a clean generated/ directory to avoid stale compiled
325
+ // artifacts (e.g. .d.ts/.js without .ts sources) from a cached extraction.
326
+ if (fs.existsSync(outputDir)) {
327
+ fs.rmSync(outputDir, { recursive: true, force: true });
328
+ }
324
329
  ensureDirectoryExists(outputDir);
325
330
  // Generate TypeScript files for ValueSets inside the embedded package
326
331
  const didGenerateValueSets = emitValueSetFiles(valueSets, outputDir, { deduplicateFilenames: true });
@@ -371,16 +376,12 @@ export async function generateIntoPackage(packageArchivePath, outArchivePath, fl
371
376
  // When client is generated, add @babelfhir-ts/client-<version> as dependency (generated client extends base)
372
377
  if (!flags?.noClient) {
373
378
  generatedPackageJson.dependencies[`@babelfhir-ts/client-${versionSlug()}`] = '^0.2.0';
374
- generatedExports['./fhir-client'] = { types: './fhir-client/index.d.ts', import: './fhir-client/index.js' };
375
379
  }
376
380
  // When zod schemas are generated, add @babelfhir-ts/zod-<version> + zod peer dependency
377
381
  if (flags?.schema === 'zod') {
378
382
  generatedPackageJson.dependencies[`@babelfhir-ts/zod-${versionSlug()}`] = '^0.1.0';
379
383
  generatedPackageJson.peerDependencies['zod'] = '^4.0.0';
380
384
  }
381
- const generatedPackageJsonPath = path.join(outputDir, 'package.json');
382
- fs.writeFileSync(generatedPackageJsonPath, JSON.stringify(generatedPackageJson, null, 2));
383
- logger.log(`Created package.json in generated folder: ${packageName}-generated@${packageVersion}`);
384
385
  copyFhirAmbientDeclaration(outputDir);
385
386
  // Generate index.ts that exports all interfaces
386
387
  logger.log('Generating index.ts exports...');
@@ -394,6 +395,13 @@ export async function generateIntoPackage(packageArchivePath, outArchivePath, fl
394
395
  });
395
396
  logger.log('FHIR client generated');
396
397
  }
398
+ // Only add ./fhir-client export if the directory was actually created
399
+ if (fs.existsSync(path.join(outputDir, 'fhir-client'))) {
400
+ generatedExports['./fhir-client'] = { types: './fhir-client/index.d.ts', import: './fhir-client/index.js' };
401
+ }
402
+ const generatedPackageJsonPath = path.join(outputDir, 'package.json');
403
+ fs.writeFileSync(generatedPackageJsonPath, JSON.stringify(generatedPackageJson, null, 2));
404
+ logger.log(`Created package.json in generated folder: ${packageName}-generated@${packageVersion}`);
397
405
  // Install base zod type stubs when --schema zod is enabled (needed for tsc compilation)
398
406
  if (flags?.schema === 'zod') {
399
407
  const { installBaseZodTypes } = await import('./emitters/zod/zodStubInstaller.js');
@@ -500,6 +508,11 @@ export async function generateIntoPackageDirect(packageArchivePath, flags) {
500
508
  valueSets = await expandValueSetsWithTx(valueSets, valueSetCodesMap, structureDefinitions, flags.txServer);
501
509
  }
502
510
  const outputDir = path.join(extractedRoot, 'generated');
511
+ // Always start with a clean generated/ directory to avoid stale compiled
512
+ // artifacts (e.g. .d.ts/.js without .ts sources) from a cached extraction.
513
+ if (fs.existsSync(outputDir)) {
514
+ fs.rmSync(outputDir, { recursive: true, force: true });
515
+ }
503
516
  ensureDirectoryExists(outputDir);
504
517
  const didGenerateValueSets = emitValueSetFiles(valueSets, outputDir, { deduplicateFilenames: true });
505
518
  if (!didGenerateValueSets)
@@ -539,15 +552,11 @@ export async function generateIntoPackageDirect(packageArchivePath, flags) {
539
552
  };
540
553
  if (!flags?.noClient) {
541
554
  generatedPackageJson.dependencies[`@babelfhir-ts/client-${versionSlug()}`] = '^0.2.0';
542
- generatedExports['./fhir-client'] = { types: './fhir-client/index.d.ts', import: './fhir-client/index.js' };
543
555
  }
544
556
  if (flags?.schema === 'zod') {
545
557
  generatedPackageJson.dependencies[`@babelfhir-ts/zod-${versionSlug()}`] = '^0.1.0';
546
558
  generatedPackageJson.peerDependencies['zod'] = '^4.0.0';
547
559
  }
548
- const generatedPackageJsonPath = path.join(outputDir, 'package.json');
549
- fs.writeFileSync(generatedPackageJsonPath, JSON.stringify(generatedPackageJson, null, 2));
550
- logger.log(`Created package.json in generated folder: ${packageName}-generated@${packageVersion}`);
551
560
  copyFhirAmbientDeclaration(outputDir);
552
561
  logger.log('Generating index.ts exports...');
553
562
  await generateIndexFile(outputDir);
@@ -557,6 +566,13 @@ export async function generateIntoPackageDirect(packageArchivePath, flags) {
557
566
  generateClient({ outputDir });
558
567
  logger.log('FHIR client generated');
559
568
  }
569
+ // Only add ./fhir-client export if the directory was actually created
570
+ if (fs.existsSync(path.join(outputDir, 'fhir-client'))) {
571
+ generatedExports['./fhir-client'] = { types: './fhir-client/index.d.ts', import: './fhir-client/index.js' };
572
+ }
573
+ const generatedPackageJsonPath = path.join(outputDir, 'package.json');
574
+ fs.writeFileSync(generatedPackageJsonPath, JSON.stringify(generatedPackageJson, null, 2));
575
+ logger.log(`Created package.json in generated folder: ${packageName}-generated@${packageVersion}`);
560
576
  if (flags?.schema === 'zod') {
561
577
  const { installBaseZodTypes } = await import('./emitters/zod/zodStubInstaller.js');
562
578
  installBaseZodTypes(outputDir);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "babelfhir-ts",
3
- "version": "1.2.3",
3
+ "version": "1.2.4",
4
4
  "description": "BabelFHIR-TS: generate TypeScript interfaces, validators, and helper classes from FHIR R4/R4B StructureDefinitions (profiles) directly inside package archives.",
5
5
  "type": "module",
6
6
  "main": "out/src/main.js",