babelfhir-ts 1.2.0 → 1.2.2
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/out/src/cli/installCommand.js +208 -0
- package/out/src/generator/emitters/validator/validatorGenerator.js +4 -3
- package/out/src/generator/emitters/validator/validatorTemplates.js +42 -2
- package/out/src/generator/index.js +21 -1
- package/out/src/generator/parser/packageParser.js +46 -0
- package/package.json +2 -1
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import { generateIntoPackage, resetFetchFailureTracking, getFetchFailureWarning } from "../generator/index.js";
|
|
2
|
+
import { getCacheConfig, clearAllCaches } from "../generator/core/cacheConfig.js";
|
|
3
|
+
import { buildQualityReport, formatReportSummary, resetDiagnostics } from "../generator/core/sdDiagnostics.js";
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import { spawn } from 'child_process';
|
|
7
|
+
/** Display all generation warnings (fetch failures, incomplete extension slices, etc.) */
|
|
8
|
+
function showGenerationWarnings() {
|
|
9
|
+
let hasWarnings = false;
|
|
10
|
+
const fetchWarning = getFetchFailureWarning();
|
|
11
|
+
if (fetchWarning) {
|
|
12
|
+
console.warn(fetchWarning);
|
|
13
|
+
hasWarnings = true;
|
|
14
|
+
}
|
|
15
|
+
return hasWarnings;
|
|
16
|
+
}
|
|
17
|
+
function cleanupCache() {
|
|
18
|
+
const cacheConfig = getCacheConfig();
|
|
19
|
+
if (fs.existsSync(cacheConfig.rootDir)) {
|
|
20
|
+
console.log('Cleaning up cache directory...');
|
|
21
|
+
clearAllCaches();
|
|
22
|
+
console.log('Cache cleaned.');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/** Detect the package manager used in the current working directory */
|
|
26
|
+
function detectPackageManager() {
|
|
27
|
+
const cwd = process.cwd();
|
|
28
|
+
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'] };
|
|
37
|
+
}
|
|
38
|
+
return { cmd: isWin ? 'npm.cmd' : 'npm', args: ['install'] };
|
|
39
|
+
}
|
|
40
|
+
function npmInstall(packagePath) {
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
const pm = detectPackageManager();
|
|
43
|
+
const pmName = path.basename(pm.cmd).replace(/\.(cmd|exe)$/, '');
|
|
44
|
+
// Check if the package already exists in package.json to avoid dependency loops.
|
|
45
|
+
const projectPkgPath = path.join(process.cwd(), 'package.json');
|
|
46
|
+
let useInstallOnly = false;
|
|
47
|
+
if (fs.existsSync(projectPkgPath)) {
|
|
48
|
+
try {
|
|
49
|
+
const tgzRelPath = './' + path.relative(process.cwd(), packagePath).replace(/\\/g, '/');
|
|
50
|
+
const projectPkg = JSON.parse(fs.readFileSync(projectPkgPath, 'utf8'));
|
|
51
|
+
const deps = projectPkg.dependencies || {};
|
|
52
|
+
for (const [name, value] of Object.entries(deps)) {
|
|
53
|
+
if (typeof value === 'string' && (value === tgzRelPath || value === packagePath)) {
|
|
54
|
+
console.log(`Package ${name} already in dependencies, running ${pmName} install`);
|
|
55
|
+
useInstallOnly = true;
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (!useInstallOnly) {
|
|
60
|
+
const tgzBaseName = path.basename(packagePath, '.tgz').replace(/^-/, '');
|
|
61
|
+
for (const name of Object.keys(deps)) {
|
|
62
|
+
if (name.replace(/[@/]/g, '-') === tgzBaseName) {
|
|
63
|
+
deps[name] = tgzRelPath;
|
|
64
|
+
projectPkg.dependencies = deps;
|
|
65
|
+
fs.writeFileSync(projectPkgPath, JSON.stringify(projectPkg, null, 2) + '\n');
|
|
66
|
+
console.log(`Updated ${name} dependency to ${tgzRelPath}`);
|
|
67
|
+
useInstallOnly = true;
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch { /* ignore, fall through to normal add */ }
|
|
74
|
+
}
|
|
75
|
+
const args = useInstallOnly ? ['install'] : [...pm.args, packagePath];
|
|
76
|
+
console.log(`Installing package with ${pmName}...`);
|
|
77
|
+
const child = spawn(pm.cmd, args, {
|
|
78
|
+
stdio: 'inherit',
|
|
79
|
+
shell: true
|
|
80
|
+
});
|
|
81
|
+
child.on('exit', (code) => {
|
|
82
|
+
if (code === 0) {
|
|
83
|
+
console.log(`✓ Package installed successfully`);
|
|
84
|
+
resolve();
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
reject(new Error(`${pmName} install failed with exit code ${code}`));
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
child.on('error', (err) => {
|
|
91
|
+
reject(new Error(`Failed to run ${pmName} install: ${err.message}`));
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
export async function handleInstallCommand(opts) {
|
|
96
|
+
const { packageToInstall, registry, generationFlags, downloadPackage } = opts;
|
|
97
|
+
// Default to cleaning cache for install command
|
|
98
|
+
if (!generationFlags.noCache) {
|
|
99
|
+
generationFlags.noCache = true;
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
let downloadedPath;
|
|
103
|
+
if (fs.existsSync(packageToInstall) && (packageToInstall.endsWith('.tgz') || packageToInstall.endsWith('.zip'))) {
|
|
104
|
+
console.log(`Using local package: ${packageToInstall}`);
|
|
105
|
+
downloadedPath = path.resolve(packageToInstall);
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
downloadedPath = await downloadPackage(packageToInstall, registry);
|
|
109
|
+
}
|
|
110
|
+
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
|
+
resetFetchFailureTracking();
|
|
114
|
+
resetDiagnostics();
|
|
115
|
+
await generateIntoPackage(downloadedPath, outputArchive, generationFlags);
|
|
116
|
+
// Clean up temp directory only if we downloaded from registry (not for local files)
|
|
117
|
+
if (!fs.existsSync(packageToInstall) || !(packageToInstall.endsWith('.tgz') || packageToInstall.endsWith('.zip'))) {
|
|
118
|
+
const tempDir = path.join(process.cwd(), '.temp-download');
|
|
119
|
+
if (fs.existsSync(tempDir)) {
|
|
120
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
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
|
|
141
|
+
const pkgJsonPath = path.join(generatedDir, 'package.json');
|
|
142
|
+
if (!fs.existsSync(pkgJsonPath)) {
|
|
143
|
+
throw new Error(`package.json not found in generated folder: ${generatedDir}`);
|
|
144
|
+
}
|
|
145
|
+
// Create a tarball from the generated folder to avoid symlink issues on Windows
|
|
146
|
+
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
|
|
147
|
+
const packageName = pkgJson.name;
|
|
148
|
+
console.log(`Creating package tarball for ${packageName}...`);
|
|
149
|
+
// Ensure ./lib exists for storing the generated tarball
|
|
150
|
+
const libDir = path.join(process.cwd(), 'lib');
|
|
151
|
+
if (!fs.existsSync(libDir)) {
|
|
152
|
+
fs.mkdirSync(libDir, { recursive: true });
|
|
153
|
+
}
|
|
154
|
+
const { create } = await import('tar');
|
|
155
|
+
const tarballName = `${packageName.replace(/[@/]/g, '-')}.tgz`;
|
|
156
|
+
const tarballPath = path.join(libDir, tarballName);
|
|
157
|
+
// npm expects tarballs to have content under a 'package/' root folder
|
|
158
|
+
await create({
|
|
159
|
+
gzip: true,
|
|
160
|
+
file: tarballPath,
|
|
161
|
+
cwd: generatedDir,
|
|
162
|
+
prefix: 'package'
|
|
163
|
+
}, ['.']);
|
|
164
|
+
console.log(`Installing ${packageName}...`);
|
|
165
|
+
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
|
+
}
|
|
175
|
+
console.log(`✓ Package tarball saved to ./lib: ${path.basename(tarballPath)}`);
|
|
176
|
+
showGenerationWarnings();
|
|
177
|
+
const report = buildQualityReport();
|
|
178
|
+
const summary = formatReportSummary(report);
|
|
179
|
+
if (summary.trim())
|
|
180
|
+
console.log(summary);
|
|
181
|
+
if (generationFlags.noCache) {
|
|
182
|
+
cleanupCache();
|
|
183
|
+
}
|
|
184
|
+
console.log(`\n✓ Package ${packageToInstall} installed and ready to use!`);
|
|
185
|
+
}
|
|
186
|
+
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);
|
|
201
|
+
}
|
|
202
|
+
if (generationFlags.noCache) {
|
|
203
|
+
cleanupCache();
|
|
204
|
+
}
|
|
205
|
+
console.error(`Error: ${error.message}`);
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -5,7 +5,7 @@ import { versionSlug } from '../../fhir/versionContext.js';
|
|
|
5
5
|
import { buildRequiredConstraints, buildMaxCardinalityConstraints, buildForbiddenFieldConstraintEntries, buildPatternValidations, } from './validatorConstraintBuilders.js';
|
|
6
6
|
import { buildNestedRequiredValidations, buildFixedValueValidations, buildProhibitedFieldValidations, buildPrimitiveFormatValidations, } from './validatorFieldBuilders.js';
|
|
7
7
|
import { buildBindingValidations } from './validatorBindingBuilder.js';
|
|
8
|
-
import { generateBundleRefValidation, generateExtensionStructuralValidation } from './validatorTemplates.js';
|
|
8
|
+
import { generateBundleRefValidation, generateContainedRefValidation, generateExtensionStructuralValidation } from './validatorTemplates.js';
|
|
9
9
|
const log = logger.withTag('validator');
|
|
10
10
|
/**
|
|
11
11
|
* Returns the content of ValidatorOptions.ts — the shared runtime options type
|
|
@@ -146,6 +146,7 @@ export function generateValidateProfileFunction(interfaceName, fields, valueSets
|
|
|
146
146
|
});
|
|
147
147
|
const primitiveFormatValidations = buildPrimitiveFormatValidations(fields);
|
|
148
148
|
const bundleRefValidation = generateBundleRefValidation(baseResourceType);
|
|
149
|
+
const containedRefValidation = generateContainedRefValidation(baseResourceType);
|
|
149
150
|
const extensionStructuralValidation = generateExtensionStructuralValidation();
|
|
150
151
|
// ── Deduplicate & filter constraints ────────────────────────────────────
|
|
151
152
|
const uniqueConstraints = Array.from(new Map(constraintsWithContext.map((item) => [item.constraint.expression + "|" + item.fieldPath, item])).values());
|
|
@@ -229,7 +230,7 @@ export async function validate${interfaceName}(resource: ${interfaceName}, optio
|
|
|
229
230
|
const errors: string[] = [];
|
|
230
231
|
const warnings: string[] = [];
|
|
231
232
|
void options;
|
|
232
|
-
${extensionStructuralValidation}
|
|
233
|
+
${extensionStructuralValidation}${containedRefValidation}
|
|
233
234
|
return { errors, warnings };
|
|
234
235
|
}`,
|
|
235
236
|
valueSetImports
|
|
@@ -265,7 +266,7 @@ ${extensionStructuralValidation}
|
|
|
265
266
|
const errors: string[] = [];
|
|
266
267
|
const warnings: string[] = [];
|
|
267
268
|
${fhirpathOptionsBlock}
|
|
268
|
-
${validationLogic}${fixedPatternValidations.join('')}${nestedRequiredValidations.join('')}${fixedValueValidations.join('')}${(bindingValidations.length > 0 || prohibitedFieldValidations.length > 0 || primitiveFormatValidations.length > 0) ? `\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const _bRes = resource as Record<string, any>;` : ''}${prohibitedFieldValidations.join('')}${bindingValidations.join('')}${primitiveFormatValidations.join('')}${sliceValidations.join('')}${extensionStructuralValidation}${bundleRefValidation}
|
|
269
|
+
${validationLogic}${fixedPatternValidations.join('')}${nestedRequiredValidations.join('')}${fixedValueValidations.join('')}${(bindingValidations.length > 0 || prohibitedFieldValidations.length > 0 || primitiveFormatValidations.length > 0) ? `\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const _bRes = resource as Record<string, any>;` : ''}${prohibitedFieldValidations.join('')}${bindingValidations.join('')}${primitiveFormatValidations.join('')}${sliceValidations.join('')}${extensionStructuralValidation}${containedRefValidation}${bundleRefValidation}
|
|
269
270
|
return { errors, warnings };
|
|
270
271
|
}`,
|
|
271
272
|
valueSetImports
|
|
@@ -31,7 +31,7 @@ export function generateBundleRefValidation(baseResourceType) {
|
|
|
31
31
|
const ref = rec.reference as string;
|
|
32
32
|
if (ref.startsWith('urn:uuid:') || ref.startsWith('urn:oid:')) {
|
|
33
33
|
if (!_fullUrls.has(ref)) {
|
|
34
|
-
errors.push('
|
|
34
|
+
errors.push('Bundle reference not found: ' + ref);
|
|
35
35
|
}
|
|
36
36
|
} else if (/^[A-Za-z]+\\//.test(ref)) {
|
|
37
37
|
if (!_resIds.has(ref)) {
|
|
@@ -40,7 +40,7 @@ export function generateBundleRefValidation(baseResourceType) {
|
|
|
40
40
|
if (_fu.endsWith('/' + ref) || _fu.endsWith(ref)) { _found = true; break; }
|
|
41
41
|
}
|
|
42
42
|
if (!_found) {
|
|
43
|
-
errors.push('
|
|
43
|
+
errors.push('Bundle reference not found: ' + ref);
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
}
|
|
@@ -56,6 +56,46 @@ export function generateBundleRefValidation(baseResourceType) {
|
|
|
56
56
|
}
|
|
57
57
|
`;
|
|
58
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Generate standalone contained reference resolution validation code.
|
|
61
|
+
* For non-Bundle resources, verifies that #fragment references resolve to contained[] entries.
|
|
62
|
+
* Bundle validation is handled separately by generateBundleRefValidation.
|
|
63
|
+
*/
|
|
64
|
+
export function generateContainedRefValidation(baseResourceType) {
|
|
65
|
+
// Bundle resources have their own reference resolution check; skip them here
|
|
66
|
+
if (baseResourceType === 'Bundle')
|
|
67
|
+
return '';
|
|
68
|
+
return `
|
|
69
|
+
// Standalone contained reference resolution: verify #id references resolve to contained[] entries
|
|
70
|
+
{
|
|
71
|
+
const _res = resource as unknown as Record<string, unknown>;
|
|
72
|
+
const _containedIds = new Set<string>();
|
|
73
|
+
if (Array.isArray(_res.contained)) {
|
|
74
|
+
for (const _c of _res.contained as Array<Record<string, unknown>>) {
|
|
75
|
+
if (_c && typeof _c.id === 'string') _containedIds.add(_c.id);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const _checkContainedRef = (obj: unknown): void => {
|
|
79
|
+
if (!obj || typeof obj !== 'object') return;
|
|
80
|
+
if (Array.isArray(obj)) { obj.forEach(_checkContainedRef); return; }
|
|
81
|
+
const _rec = obj as Record<string, unknown>;
|
|
82
|
+
if (typeof _rec.reference === 'string') {
|
|
83
|
+
const _ref = _rec.reference as string;
|
|
84
|
+
if (_ref.startsWith('#')) {
|
|
85
|
+
const _id = _ref.substring(1);
|
|
86
|
+
if (_id && !_containedIds.has(_id)) {
|
|
87
|
+
errors.push('Contained reference not found: ' + _ref);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
for (const [_k, _v] of Object.entries(_rec)) {
|
|
92
|
+
if (_k !== 'contained') _checkContainedRef(_v);
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
_checkContainedRef(_res);
|
|
96
|
+
}
|
|
97
|
+
`;
|
|
98
|
+
}
|
|
59
99
|
/**
|
|
60
100
|
* Generate extension structural validation code.
|
|
61
101
|
* Checks extension.url required, ext-1 constraint, empty objects.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from 'path';
|
|
2
2
|
import fs from 'fs';
|
|
3
3
|
import { fileURLToPath } from 'url';
|
|
4
|
-
import { extractPackage, readStructureDefinitionsFromDir, createPackageFromDir, readValueSetCodesWithDependencies, readValueSetsFromDir, detectFhirVersion, ensureDependenciesDownloaded } from './parser/packageParser.js';
|
|
4
|
+
import { extractPackage, readStructureDefinitionsFromDir, readStructureDefinitionsFromDependencies, createPackageFromDir, readValueSetCodesWithDependencies, readValueSetsFromDir, detectFhirVersion, ensureDependenciesDownloaded } from './parser/packageParser.js';
|
|
5
5
|
import { fetchStructureDefinitions, fetchStructureDefinition, registerLocalStructureDefinitions, clearLocalStructureDefinitions, collectValueSetBindingUrls } from './parser/sdParser.js';
|
|
6
6
|
import { ensureDirectoryExists, downloadFile } from './core/utils.js';
|
|
7
7
|
import { getFhirPackagesCacheDir } from './core/cacheConfig.js';
|
|
@@ -65,6 +65,7 @@ async function compileTypeScriptToJS(dir) {
|
|
|
65
65
|
allowSyntheticDefaultImports: true,
|
|
66
66
|
strict: false,
|
|
67
67
|
resolveJsonModule: true,
|
|
68
|
+
typeRoots: [path.join(dir, 'node_modules', '@types')],
|
|
68
69
|
types: ['node']
|
|
69
70
|
},
|
|
70
71
|
include: [`${dir}/**/*.ts`],
|
|
@@ -235,6 +236,9 @@ export async function generate(fhirSource, outputDir, flags) {
|
|
|
235
236
|
structureDefinitions = readStructureDefinitionsFromDir(extracted);
|
|
236
237
|
// Ensure dependency packages are downloaded before loading ValueSets
|
|
237
238
|
await ensureDependenciesDownloaded(extracted);
|
|
239
|
+
// Register SDs from dependency packages so external profile resolution can find them
|
|
240
|
+
const depSDs = readStructureDefinitionsFromDependencies(extracted);
|
|
241
|
+
registerLocalStructureDefinitions(depSDs);
|
|
238
242
|
// Load ValueSets from package AND its dependencies (for binding resolution)
|
|
239
243
|
valueSetCodesMap = readValueSetCodesWithDependencies(extracted);
|
|
240
244
|
valueSets = readValueSetsFromDir(extracted);
|
|
@@ -326,6 +330,9 @@ export async function generateIntoPackage(packageArchivePath, outArchivePath, fl
|
|
|
326
330
|
const { existingStructureDefinitions, profileIdToName, profileUrlToName } = buildProfileRegistries(structureDefinitions, fhirInterfaceNames);
|
|
327
331
|
// Register all local StructureDefinitions for resolution before HTTP fetches
|
|
328
332
|
registerLocalStructureDefinitions(structureDefinitions);
|
|
333
|
+
// Register SDs from dependency packages so external profile resolution can find them
|
|
334
|
+
const depSDs = readStructureDefinitionsFromDependencies(extractedRoot);
|
|
335
|
+
registerLocalStructureDefinitions(depSDs);
|
|
329
336
|
for (const sd of structureDefinitions) {
|
|
330
337
|
await processStructureDefinition(sd, { outputDir, fhirSourceHint: '', valueSetCodesMap, valueSets, existingStructureDefinitions, profileIdToName, profileUrlToName, flags });
|
|
331
338
|
}
|
|
@@ -390,11 +397,21 @@ export async function generateIntoPackage(packageArchivePath, outArchivePath, fl
|
|
|
390
397
|
// Install fhirpath type stub so tsc can resolve validator imports
|
|
391
398
|
const { installFhirpathStub, removeFhirpathStub } = await import('./emitters/validator/fhirpathStubInstaller.js');
|
|
392
399
|
installFhirpathStub(outputDir);
|
|
400
|
+
// Install minimal @types/node stub so tsc can resolve `import { createRequire } from 'module'`
|
|
401
|
+
const nodeTypesDir = path.join(outputDir, 'node_modules', '@types', 'node');
|
|
402
|
+
fs.mkdirSync(nodeTypesDir, { recursive: true });
|
|
403
|
+
fs.writeFileSync(path.join(nodeTypesDir, 'index.d.ts'), `declare module 'module' {\n export function createRequire(filename: string | URL): NodeRequire;\n}\n`);
|
|
404
|
+
fs.writeFileSync(path.join(nodeTypesDir, 'package.json'), JSON.stringify({ name: '@types/node', version: '0.0.0-stub', types: 'index.d.ts' }));
|
|
393
405
|
// Compile TypeScript to JavaScript
|
|
394
406
|
logger.log('Compiling TypeScript to JavaScript...');
|
|
395
407
|
await compileTypeScriptToJS(outputDir);
|
|
396
408
|
// Remove fhirpath stub — the real package is a peer dependency
|
|
397
409
|
removeFhirpathStub(outputDir);
|
|
410
|
+
// Remove @types/node stub — only needed for compilation
|
|
411
|
+
try {
|
|
412
|
+
fs.rmSync(path.join(outputDir, 'node_modules', '@types'), { recursive: true, force: true });
|
|
413
|
+
}
|
|
414
|
+
catch { /* ignore */ }
|
|
398
415
|
// Remove base client type stubs — they were only needed for tsc to resolve
|
|
399
416
|
// @babelfhir-ts/client-<version> imports during compilation. The real package is
|
|
400
417
|
// installed by the consumer via npm.
|
|
@@ -467,6 +484,9 @@ export async function generateForDirectory(inputDir, outputDir, flags) {
|
|
|
467
484
|
const valueSets = readValueSetsFromDir(inputDir);
|
|
468
485
|
// Ensure dependency packages are downloaded before loading ValueSets
|
|
469
486
|
await ensureDependenciesDownloaded(inputDir);
|
|
487
|
+
// Register SDs from dependency packages so external profile resolution can find them
|
|
488
|
+
const depSDs = readStructureDefinitionsFromDependencies(inputDir);
|
|
489
|
+
registerLocalStructureDefinitions(depSDs);
|
|
470
490
|
const valueSetCodesMap = readValueSetCodesWithDependencies(inputDir);
|
|
471
491
|
logger.log(`Loaded ${valueSets.size} ValueSets from ${inputDir} (${Array.from(valueSets.values()).filter(vs => vs.isSmall).length} suitable for union types)`);
|
|
472
492
|
const entries = fs.readdirSync(inputDir);
|
|
@@ -521,6 +521,52 @@ export async function ensureDependenciesDownloaded(extractedRoot) {
|
|
|
521
521
|
}
|
|
522
522
|
await walkDeps(extractedRoot);
|
|
523
523
|
}
|
|
524
|
+
/**
|
|
525
|
+
* Reads StructureDefinitions from all dependency packages (recursively).
|
|
526
|
+
* Returns SDs from dependencies only — the main package SDs are loaded separately.
|
|
527
|
+
* This allows external profile resolution to find extension SDs from dependency packages
|
|
528
|
+
* (e.g., hl7.fhir.uv.extensions) without HTTP fetching.
|
|
529
|
+
*/
|
|
530
|
+
export function readStructureDefinitionsFromDependencies(extractedRoot) {
|
|
531
|
+
const cacheDir = getFhirPackagesCacheDir();
|
|
532
|
+
const visited = new Set();
|
|
533
|
+
const allSDs = [];
|
|
534
|
+
function walkDeps(pkgDir, isRoot) {
|
|
535
|
+
const pkgDirNorm = path.normalize(pkgDir);
|
|
536
|
+
if (visited.has(pkgDirNorm))
|
|
537
|
+
return;
|
|
538
|
+
visited.add(pkgDirNorm);
|
|
539
|
+
// Read SDs from this package (skip the root — those are loaded separately)
|
|
540
|
+
if (!isRoot) {
|
|
541
|
+
const sds = readStructureDefinitionsFromDir(pkgDir);
|
|
542
|
+
allSDs.push(...sds);
|
|
543
|
+
}
|
|
544
|
+
// Find package.json and recurse into dependencies
|
|
545
|
+
const packageJsonPath = path.join(pkgDir, 'package', 'package.json');
|
|
546
|
+
const altPath = path.join(pkgDir, 'package.json');
|
|
547
|
+
const pkgJsonPath = fs.existsSync(packageJsonPath) ? packageJsonPath
|
|
548
|
+
: fs.existsSync(altPath) ? altPath : null;
|
|
549
|
+
if (!pkgJsonPath)
|
|
550
|
+
return;
|
|
551
|
+
try {
|
|
552
|
+
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8'));
|
|
553
|
+
const deps = pkgJson.dependencies || {};
|
|
554
|
+
for (const [depName, depVersion] of Object.entries(deps)) {
|
|
555
|
+
for (const sep of ['@', '#']) {
|
|
556
|
+
const depDir = path.join(cacheDir, `${depName}${sep}${depVersion}`);
|
|
557
|
+
if (fs.existsSync(depDir)) {
|
|
558
|
+
walkDeps(depDir, false);
|
|
559
|
+
break;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
catch { /* skip */ }
|
|
565
|
+
}
|
|
566
|
+
walkDeps(extractedRoot, true);
|
|
567
|
+
log.info(`Loaded ${allSDs.length} StructureDefinitions from dependency packages`);
|
|
568
|
+
return allSDs;
|
|
569
|
+
}
|
|
524
570
|
/**
|
|
525
571
|
* Reads ValueSet codes from a package and all its dependencies (recursively).
|
|
526
572
|
* Looks for dependency packages in the FHIR package cache directory.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "babelfhir-ts",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.2",
|
|
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",
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"out/src/generator/",
|
|
12
|
+
"out/src/cli/",
|
|
12
13
|
"out/src/main.js",
|
|
13
14
|
"out/src/logger.js",
|
|
14
15
|
"out/fhir-r4.d.ts",
|