pcf-scripts
Version:
This package contains a module for building PowerApps Component Framework (PCF) controls. See project homepage how to install.
303 lines (301 loc) • 14.3 kB
JavaScript
"use strict";
// Copyright (C) Microsoft Corporation. All rights reserved.
Object.defineProperty(exports, "__esModule", { value: true });
exports.ManifestTypesGenerator = void 0;
const semver_1 = require("semver");
const ts = require("typescript");
/* eslint-disable @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-return,@typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access */
// the following props are compatible number properties that can be grouped together in the same type group
const numberProperties = [
"ComponentFramework.PropertyTypes.NumberProperty",
"ComponentFramework.PropertyTypes.WholeNumberProperty",
"ComponentFramework.PropertyTypes.DecimalNumberProperty",
"ComponentFramework.PropertyTypes.FloatingNumberProperty",
];
class ManifestTypesGenerator {
constructor() {
// map each type group's name to its type name
this.typeGroups = {};
this.outputs = {};
// map each property name defined in manifest to its type name
this.parameters = {};
this.dataSetNames = [];
this.internalImportsRequired = {};
}
getFieldType(type) {
const prefix = type.split(".")[0];
switch (prefix) {
case "SingleLine":
case "Multiple":
return "ComponentFramework.PropertyTypes.StringProperty";
case "Whole":
return "ComponentFramework.PropertyTypes.WholeNumberProperty";
case "TwoOptions":
return "ComponentFramework.PropertyTypes.TwoOptionsProperty";
case "DateAndTime":
return "ComponentFramework.PropertyTypes.DateTimeProperty";
case "Decimal":
return "ComponentFramework.PropertyTypes.DecimalNumberProperty";
case "FP":
return "ComponentFramework.PropertyTypes.FloatingNumberProperty";
case "Currency":
return "ComponentFramework.PropertyTypes.NumberProperty";
case "OptionSet":
return "ComponentFramework.PropertyTypes.OptionSetProperty";
case "Lookup":
return "ComponentFramework.PropertyTypes.LookupProperty";
case "MultiSelectOptionSet":
return "ComponentFramework.PropertyTypes.MultiSelectOptionSetProperty";
default:
return this.handleInternalTypes(prefix) ?? "ComponentFramework.PropertyTypes.Property";
}
}
handleInternalTypes(prefix) {
const result = undefined;
let interfaceName;
switch (prefix) {
case "File":
return "any";
default:
}
if (result) {
// eslint-disable-next-line no-prototype-builtins
if (!this.internalImportsRequired.hasOwnProperty(interfaceName)) {
this.internalImportsRequired[interfaceName] = [];
}
if (!this.internalImportsRequired[interfaceName].includes(result)) {
this.internalImportsRequired[interfaceName].push(result);
}
}
return result;
}
getTypeGroupType(tg) {
let fieldType = "";
let nextType;
for (const type of tg.type) {
// first type in the type group, set the base
if (fieldType === "") {
fieldType = this.getFieldType(type);
}
else {
// else check whether the next type in the type group is compatible
nextType = this.getFieldType(type);
if (nextType !== fieldType) {
// incompatible types might all be NumberProperty
if (numberProperties.includes(nextType) && numberProperties.includes(fieldType)) {
fieldType = "ComponentFramework.PropertyTypes.NumberProperty";
}
else {
fieldType = "ComponentFramework.PropertyTypes.Property";
break;
}
}
}
}
return fieldType;
}
// Get the type for props under IOutput interface (string, number, boolean or any)
getOutputFieldType(type) {
// Enum is always treated as string
if (type.includes("ComponentFramework.PropertyTypes.EnumProperty")) {
return ts.SyntaxKind.StringKeyword;
}
switch (type) {
case "ComponentFramework.PropertyTypes.StringProperty":
return ts.SyntaxKind.StringKeyword;
case "ComponentFramework.PropertyTypes.NumberProperty":
case "ComponentFramework.PropertyTypes.WholeNumberProperty":
case "ComponentFramework.PropertyTypes.DecimalNumberProperty":
case "ComponentFramework.PropertyTypes.FloatingNumberProperty":
case "ComponentFramework.PropertyTypes.OptionSetProperty":
return ts.SyntaxKind.NumberKeyword;
case "ComponentFramework.PropertyTypes.TwoOptionsProperty":
return ts.SyntaxKind.BooleanKeyword;
default:
return ts.SyntaxKind.AnyKeyword;
}
}
getEnumType(propNode) {
if (!propNode.value) {
return "ComponentFramework.PropertyTypes.EnumProperty<any>";
}
let enumType = "";
let first = true;
const values = propNode.value;
for (const val of values) {
if (!first) {
enumType += " | ";
}
else {
first = false;
}
enumType += `"${val._}"`;
}
return `ComponentFramework.PropertyTypes.EnumProperty<${enumType}>`;
}
parseTypeGroupNode(typeGroups) {
if (typeGroups) {
for (const tg of typeGroups) {
const typeGroupName = tg.$.name;
const typeGroupType = this.getTypeGroupType(tg);
this.typeGroups[typeGroupName] = typeGroupType;
}
}
}
parsePropertyNodes(props) {
if (!props) {
return;
}
for (const prop of props) {
const propName = prop.$.name;
let propType;
// check if usage is defined; ignore those without a usage
if (prop.$.usage) {
if (prop.$["of-type-group"]) {
if (prop.$["of-type-group"] in this.typeGroups) {
propType = this.typeGroups[prop.$["of-type-group"]];
}
else {
throw new Error(`Type group ${prop.$["of-type-group"]} not found.`);
}
}
else if (prop.$["of-type"]) {
propType = prop.$["of-type"] === "Enum" ? this.getEnumType(prop) : this.getFieldType(prop.$["of-type"]);
}
else {
propType = "ComponentFramework.PropertyTypes.Property";
}
if (prop.$.usage === "output" || prop.$.usage === "bound") {
this.outputs[propName] = propType;
}
if (prop.$.usage === "input" || prop.$.usage === "bound") {
this.parameters[propName] = propType;
}
}
}
}
parseDataSetNodes(dataSets) {
if (!dataSets) {
return;
}
this.dataSetNames = dataSets.map((set) => set.$.name);
}
// Helper for making the AST of ManifestTypes.d.ts - create all properties under IInput interface
createInputProperties() {
const inputProperties = Object.entries(this.parameters).map(([propName, propValue]) => {
const typeRef = this.getTypescriptCompilerApi().createTypeReferenceNode(/*typeName*/ propValue, /*typeArguments*/ undefined);
return this.getTypescriptCompilerApi().createPropertySignature(
/*modifiers*/ undefined,
/*name*/ propName,
/*questionToken*/ undefined,
/*type*/ typeRef);
});
const dataSetTypeRef = this.getTypescriptCompilerApi().createTypeReferenceNode(
/*typeName*/ "ComponentFramework.PropertyTypes.DataSet",
/*typeArguments*/ undefined);
const dataSetsProperties = this.dataSetNames.map((name) => {
return this.getTypescriptCompilerApi().createPropertySignature(
/*modifiers*/ undefined,
/*name*/ name,
/*questionToken*/ undefined,
/*type*/ dataSetTypeRef);
});
return inputProperties.concat(dataSetsProperties);
}
// Helper for making the AST of ManifestTypes.d.ts - create all properties under IOutput interface
createOutputProperties() {
const outputProperties = Object.keys(this.outputs).map((propName) => {
let typeRef;
// DateTimeProperty is a special case here - it doesn't have a keyword type defined in the API
if (this.outputs[propName] === "ComponentFramework.PropertyTypes.DateTimeProperty") {
typeRef = this.getTypescriptCompilerApi().createTypeReferenceNode(/*typeName*/ "Date", /*typeArguments*/ undefined);
}
else if (this.outputs[propName] === "any") {
typeRef = this.getTypescriptCompilerApi().createTypeReferenceNode(/*typeName*/ "any", /*typeArguments*/ undefined);
}
else if (this.outputs[propName] === "ComponentFramework.PropertyTypes.LookupProperty") {
typeRef = this.getTypescriptCompilerApi().createTypeReferenceNode(/*typeName*/ "ComponentFramework.LookupValue[]", /*typeArguments*/ undefined);
}
else if (this.outputs[propName] === "ComponentFramework.PropertyTypes.MultiSelectOptionSetProperty") {
typeRef = this.getTypescriptCompilerApi().createTypeReferenceNode(/*typeName*/ "number[]", /*typeArguments*/ undefined);
}
else {
const keyword = this.getOutputFieldType(this.outputs[propName]);
typeRef = this.getTypescriptCompilerApi().createKeywordTypeNode(keyword);
}
return this.getTypescriptCompilerApi().createPropertySignature(
/*modifiers*/ undefined,
/*name*/ propName,
/*questionToken*/ this.getTypescriptCompilerApi().createToken(ts.SyntaxKind.QuestionToken),
/*type*/ typeRef);
});
return outputProperties;
}
// Construct the full AST of ManifestTypes.d.ts
makeAST() {
const getArgs = (name) => {
const args = [
/*modifiers*/ [this.getTypescriptCompilerApi().createToken(ts.SyntaxKind.ExportKeyword)],
/*name*/ name,
/*typeParameters*/ undefined,
/*heritageClauses*/ undefined,
/*members*/ name === "IInputs" ? this.createInputProperties() : this.createOutputProperties(),
];
// createInterfaceDeclaration has a different interface on Typescript 5+ and deprecated since 4.8.0
if (!ts.version || (0, semver_1.lt)(ts.version, "4.8.0")) {
// Add decorators as the first argument for typescript versions < 4.8.0
return [/*decorators*/ undefined, ...args];
}
return args;
};
const inputsInterface = this.getTypescriptCompilerApi().createInterfaceDeclaration(...getArgs("IInputs"));
const outputsInterface = this.getTypescriptCompilerApi().createInterfaceDeclaration(...getArgs("IOutputs"));
const importsToAdd = [];
for (const typeNS in this.internalImportsRequired) {
// eslint-disable-next-line no-prototype-builtins
if (this.internalImportsRequired.hasOwnProperty(typeNS)) {
const properties = this.internalImportsRequired[typeNS];
const importSpecifiers = [];
properties.forEach((property) => {
importSpecifiers.push(this.getTypescriptCompilerApi().createImportSpecifier(undefined, this.getTypescriptCompilerApi().createIdentifier(property)));
});
importsToAdd.push(this.getTypescriptCompilerApi().createImportDeclaration(undefined, undefined, this.getTypescriptCompilerApi().createImportClause(false, undefined, this.getTypescriptCompilerApi().createNamedImports(importSpecifiers)), this.getTypescriptCompilerApi().createStringLiteral(typeNS)));
}
}
return this.getTypescriptCompilerApi().createNodeArray([...importsToAdd, inputsInterface, outputsInterface], false);
}
// Use Typescript's printer API to generate the actual code from the AST
generateManifestTypes(manifestObj) {
this.parseTypeGroupNode(manifestObj.manifest.control["type-group"]);
this.parsePropertyNodes(manifestObj.manifest.control.property);
this.parseDataSetNodes(manifestObj.manifest.control["data-set"]);
const resultFile = ts.createSourceFile(
/*fileName*/ "output.ts",
/*sourceText*/ "",
/*languageVersion*/ ts.ScriptTarget.Latest,
/*setParentNodes*/ false,
/*scriptKind*/ ts.ScriptKind.TS);
const printer = ts.createPrinter({
newLine: ts.NewLineKind.LineFeed,
});
const result = printer.printList(ts.ListFormat.MultiLine, this.makeAST(), resultFile);
return result;
}
/**
* This method is used to make typescript compiler API available in
* typescript versions < 4.2.4 and above 4.2.4
* Typescript < 4.2.4 requires 'ts' and above requires 'ts.factory'
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getTypescriptCompilerApi() {
if (ts.factory !== undefined) {
return ts.factory;
}
else {
return ts;
}
}
}
exports.ManifestTypesGenerator = ManifestTypesGenerator;
/* eslint-enable @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-return,@typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access */
//# sourceMappingURL=manifestTypesGenerator.js.map