Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 25 additions & 4 deletions biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,24 +34,42 @@
"noImplicitAnyLet": "off",
"noExplicitAny": "off",
"noAssignInExpressions": "off",
"noVar": "error"
"noVar": "error",
"noEmptyBlockStatements": "warn",
"noMisplacedAssertion": "warn"
},
"correctness": {
"noUnusedPrivateClassMembers": "error",
"noUnusedImports": { "level": "warn" }
"noUnusedImports": { "level": "warn" },
"noUnusedVariables": "error",
"noUnusedFunctionParameters": "error"
},
"complexity": {
"noExcessiveCognitiveComplexity": {
"level": "warn",
"options": {
"maxAllowedComplexity": 41
}
}
},
"noUselessFragments": "error"
},
"style": {
"useConsistentArrayType": "error",
"useArrayLiterals": "error",
"noNestedTernary": "error"
"noNestedTernary": "error",
"useImportType": "error",
"useExportType": "error",
"useThrowOnlyError": "error",
"useDefaultSwitchClause": "error",
"noEnum": "error",
"useFilenamingConvention": "error",
"noUselessElse": "warn"
},
"performance": {
"useTopLevelRegex": "warn"
},
"nursery": {
"noFloatingPromises": "warn"
},
"recommended": true
}
Expand All @@ -69,6 +87,9 @@
"rules": {
"style": {
"noNonNullAssertion": "off"
},
"performance": {
"useTopLevelRegex": "off"
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/api/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -437,7 +437,7 @@ export class APIBuilder {

this.logger.debug(`Starting generation with ${this.generators.length} generators`);
try {
if (this.options.cleanOutput) cleanup(this.options, this.logger);
if (this.options.cleanOutput) await cleanup(this.options, this.logger);

let register: Register;
if (this.prebuiltRegister) {
Expand Down
7 changes: 4 additions & 3 deletions src/api/writer-generator/csharp/csharp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,8 @@ const resolveCSharpAssets = (fn: string) => {
const __dirname = Path.dirname(__filename);
if (__filename.endsWith("dist/index.js")) {
return Path.resolve(__dirname, "..", "assets", "api", "writer-generator", "csharp", fn);
} else {
return Path.resolve(__dirname, "../../../..", "assets", "api", "writer-generator", "csharp", fn);
}
return Path.resolve(__dirname, "../../../..", "assets", "api", "writer-generator", "csharp", fn);
};

const PRIMITIVE_TYPE_MAP: Record<string, string> = {
Expand Down Expand Up @@ -63,13 +62,15 @@ const formatBaseClass = (schema: SpecializationTypeSchema | NestedTypeSchema) =>
return schema.base ? `: ${schema.base.name}` : "";
};

const LEADING_DIGIT_RE = /^\d/;

const canonicalToName = (canonical: string | undefined, dropFragment = true): string | undefined => {
if (!canonical) return undefined;
let localName = canonical.split("/").pop();
if (!localName) return undefined;
if (dropFragment && localName.includes("#")) localName = localName.split("#")[0];
if (!localName) return undefined;
if (/^\d/.test(localName)) {
if (LEADING_DIGIT_RE.test(localName)) {
localName = `number_${localName}`;
}
return formatName(localName);
Expand Down
10 changes: 5 additions & 5 deletions src/api/writer-generator/introspection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export class IntrospectionWriter extends FileSystemWriter<IntrospectionWriterOpt

if (this.opts.typeSchemas) {
if (Path.extname(this.opts.typeSchemas) === ".ndjson") {
this.writeNdjson(tsIndex.schemas, this.opts.typeSchemas, typeSchemaToJson);
await this.writeNdjson(tsIndex.schemas, this.opts.typeSchemas, typeSchemaToJson);
} else {
const items = tsIndex.schemas.map((ts) => typeSchemaToJson(ts, true));
const seenFilenames = new Set<string>();
Expand Down Expand Up @@ -125,9 +125,9 @@ export class IntrospectionWriter extends FileSystemWriter<IntrospectionWriterOpt
});

if (Path.extname(outputPath) === ".ndjson") {
this.writeNdjson(fhirSchemas, outputPath, fhirSchemaToJson);
await this.writeNdjson(fhirSchemas, outputPath, fhirSchemaToJson);
} else {
this.writeJsonFiles(
await this.writeJsonFiles(
fhirSchemas.map((fs) => fhirSchemaToJson(fs, true)),
outputPath,
);
Expand All @@ -148,9 +148,9 @@ export class IntrospectionWriter extends FileSystemWriter<IntrospectionWriterOpt
});

if (Path.extname(outputPath) === ".ndjson") {
this.writeNdjson(structureDefinitions, outputPath, structureDefinitionToJson);
await this.writeNdjson(structureDefinitions, outputPath, structureDefinitionToJson);
} else {
this.writeJsonFiles(
await this.writeJsonFiles(
structureDefinitions.map((sd) => structureDefinitionToJson(sd, true)),
outputPath,
);
Expand Down
4 changes: 3 additions & 1 deletion src/api/writer-generator/mustache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ export function loadMustacheGeneratorConfig(
if (parsed && typeof parsed === "object") {
return parsed as Partial<FileBasedMustacheGeneratorOptions>;
}
} catch (_e) {}
} catch {
// Missing or invalid config.json: fall through to the warning below and return defaults.
}
logger?.warn(`Failed to load JSON file with mustache generator config: ${filePath}`);
return {};
}
Expand Down
4 changes: 3 additions & 1 deletion src/api/writer-generator/typescript/profile-extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,10 @@ export const extractValueField = (elements: string[] | undefined): string | unde
/**
* Map a FHIR value field name (e.g. "valueCoding") to its TypeScript type.
*/
const VALUE_PREFIX_RE = /^value/;

export const valueFieldToTsType = (valueField: string): string => {
const fhirName = valueField.replace(/^value/, "");
const fhirName = valueField.replace(VALUE_PREFIX_RE, "");
// Primitive types that map to TS primitives
const primitives: Record<string, string> = {
String: "string",
Expand Down
4 changes: 3 additions & 1 deletion src/api/writer-generator/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
const WORD_SPLIT_RE = /(?<=[a-z])(?=[A-Z])|[-_.\s]/;

export const words = (s: string) => {
return s.split(/(?<=[a-z])(?=[A-Z])|[-_.\s]/).filter(Boolean);
return s.split(WORD_SPLIT_RE).filter(Boolean);
};

export const kebabCase = (s: string) => {
Expand Down
3 changes: 1 addition & 2 deletions src/api/writer-generator/writer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,9 +215,8 @@ export abstract class Writer<T extends WriterOptions = WriterOptions> extends Fi
tokens = tokens.map((token) => {
if (typeof token === "string") {
return token;
} else {
return JSON.stringify(token, null, 2);
}
return JSON.stringify(token, null, 2);
});
this.comment(...tokens);
}
Expand Down
24 changes: 13 additions & 11 deletions src/typeschema/core/field-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,8 @@ export function buildFieldType(
.slice(1) // drop canonicalUrl
.filter((_, i) => i % 2 === 1); // drop `elements` from path
return mkNestedIdentifier(register, fhirSchema, refPath);
} else if (element.type) {
}
if (element.type) {
const url = register.ensureSpecializationCanonicalUrl(element.type);
const fieldFs = register.resolveFs(fhirSchema.package_meta, url);
if (!fieldFs) {
Expand All @@ -326,19 +327,20 @@ export function buildFieldType(
);
}
return mkIdentifier(fieldFs);
} else if (element.choices) {
}
if (element.choices) {
return undefined;
} else if (fhirSchema.derivation === "constraint") {
}
if (fhirSchema.derivation === "constraint") {
return undefined; // FIXME: should be removed
} else {
// Some packages (e.g., simplifier.core.r4.*) have incomplete element definitions
// Log a warning but continue processing instead of throwing
logger?.dryWarn(
"#fieldTypeNotFound",
`Can't recognize element type: <${fhirSchema.url}>.${path.join(".")} (pkg: '${packageMetaToFhir(fhirSchema.package_meta)}'): missing type info`,
);
return undefined;
}
// Some packages (e.g., simplifier.core.r4.*) have incomplete element definitions
// Log a warning but continue processing instead of throwing
logger?.dryWarn(
"#fieldTypeNotFound",
`Can't recognize element type: <${fhirSchema.url}>.${path.join(".")} (pkg: '${packageMetaToFhir(fhirSchema.package_meta)}'): missing type info`,
);
return undefined;
}

export const mkField = (
Expand Down
7 changes: 5 additions & 2 deletions src/typeschema/core/identifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,16 @@ export function mkIdentifier(fhirSchema: RichFHIRSchema): Identifier {
return { kind: "resource", ...fields };
}

const VALUE_SET_NAME_SPLIT_RE = /[-_]/;
const OPAQUE_VALUE_SET_ID_RE = /^[a-zA-Z0-9_-]{20,}$/;

const getValueSetName = (url: CanonicalUrl): Name => {
const urlParts = url.split("/");
const lastSegment = urlParts[urlParts.length - 1];

if (lastSegment && lastSegment.length > 0) {
return lastSegment
.split(/[-_]/)
.split(VALUE_SET_NAME_SPLIT_RE)
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join("") as Name;
}
Expand Down Expand Up @@ -97,7 +100,7 @@ export function mkValueSetIdentifierByUrl(
const valueSet: RichValueSet = register.resolveVs(pkg, valueSetUrl) || valuesSetFallback;
// NOTE: ignore valueSet.name due to human name
const valueSetName: Name =
valueSet?.id && !/^[a-zA-Z0-9_-]{20,}$/.test(valueSet.id) ? (valueSet.id as Name) : valueSetNameFallback;
valueSet?.id && !OPAQUE_VALUE_SET_ID_RE.test(valueSet.id) ? (valueSet.id as Name) : valueSetNameFallback;

return {
kind: "value-set",
Expand Down
3 changes: 1 addition & 2 deletions src/typeschema/ir/tree-shake.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,8 @@ import type { IrReport, TreeShakeConf, TreeShakeReport, TreeShakeRule } from "./
const ensureIrReport = (indexOrReport: TypeSchemaIndex | IrReport): IrReport => {
if ("irReport" in indexOrReport && typeof indexOrReport.irReport === "function") {
return indexOrReport.irReport();
} else {
return indexOrReport as IrReport;
}
return indexOrReport as IrReport;
};

export const rootTreeShakeReadme = (report: TypeSchemaIndex | IrReport) => {
Expand Down
4 changes: 3 additions & 1 deletion src/typeschema/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import type {
} from "@typeschema/types";
import { enrichFHIRSchema, enrichValueSet, packageMetaToFhir, packageMetaToNpm } from "@typeschema/types";

const BARE_RESOURCE_NAME_RE = /^[a-zA-Z0-9]+$/;

export type Register = {
testAppendFs(fs: FHIRSchema): void;
ensureSpecializationCanonicalUrl(name: string | Name | CanonicalUrl): CanonicalUrl;
Expand Down Expand Up @@ -224,7 +226,7 @@ export const registerFromManager = async (
const ensureSpecializationCanonicalUrl = (name: string | Name | CanonicalUrl): CanonicalUrl => {
// Strip version suffix from canonical URL (e.g., "Extension|4.0.1" -> "Extension")
if (name.includes("|")) name = name.split("|")[0] as CanonicalUrl;
if (name.match(/^[a-zA-Z0-9]+$/)) {
if (BARE_RESOURCE_NAME_RE.test(name)) {
return `http://hl7.org/fhir/StructureDefinition/${name}` as CanonicalUrl;
}
return name as CanonicalUrl;
Expand Down
4 changes: 3 additions & 1 deletion src/typeschema/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,16 @@ import type { StructureDefinition, ValueSet, ValueSetCompose } from "@root/fhir-
export type Name = string & { readonly __brand: unique symbol };
export type CanonicalUrl = string & { readonly __brand: unique symbol };

const LEADING_DIGIT_RE = /^\d/;

export const extractNameFromCanonical = (canonical: CanonicalUrl, dropFragment = true) => {
let localName = canonical.split("/").pop();
if (!localName) return undefined;
if (dropFragment && localName.includes("#")) {
localName = localName.split("#")[0];
}
if (!localName) return undefined;
if (/^\d/.test(localName)) {
if (LEADING_DIGIT_RE.test(localName)) {
localName = `number_${localName}`;
}
return localName;
Expand Down
Loading