@kinvolk/headlamp-plugin
Version:
The needed infrastructure for building Headlamp plugins.
233 lines (232 loc) • 9.3 kB
JavaScript
/*
* Copyright 2025 The Kubernetes Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
const MAX_STRING_LITERALS = 200; // Max distinct strings to use literal types
const MAX_STRING_LITERAL_LENGTH = 20;
function createTypeInfoNode() {
return {
primitiveTypes: new Set(),
stringLiterals: new Set(), // Initialize the new set
hasStringType: false, // Initialize flag
isArray: false,
arrayElementInfo: null,
isObject: false,
objectProperties: new Map(),
presenceCount: 0,
parentObjectCount: 0,
};
}
/**
* Recursively aggregates type information from a value into a TypeInfoNode.
* Records string literals if applicable.
*/
function aggregateTypeInfo(value, node, parentObjectCount) {
node.presenceCount++;
node.parentObjectCount = parentObjectCount;
const type = typeof value;
if (value === null) {
node.primitiveTypes.add('null');
}
else if (type === 'string') {
node.hasStringType = true;
// Add to primitive types initially (might be replaced by literals later)
node.primitiveTypes.add('string');
// Add the literal value if within reasonable limits for tracking
// Avoid storing excessive amounts of unique strings if the threshold is eventually exceeded
if (node.stringLiterals.size < MAX_STRING_LITERALS) {
if (value.length < MAX_STRING_LITERAL_LENGTH) {
node.stringLiterals.add(value);
}
}
else {
node.stringLiterals.clear();
}
}
else if (type === 'number') {
node.primitiveTypes.add('number');
}
else if (type === 'boolean') {
node.primitiveTypes.add('boolean');
}
else if (Array.isArray(value)) {
node.isArray = true;
if (!node.arrayElementInfo) {
node.arrayElementInfo = createTypeInfoNode();
}
const arrayPresenceCount = node.presenceCount;
value.forEach(element => {
aggregateTypeInfo(element, node.arrayElementInfo, arrayPresenceCount);
});
if (node.arrayElementInfo)
node.arrayElementInfo.parentObjectCount = arrayPresenceCount;
}
else if (type === 'object') {
node.isObject = true;
const numObjectOccurrences = node.presenceCount;
const currentKeys = new Set();
for (const key in value) {
if (Object.prototype.hasOwnProperty.call(value, key)) {
currentKeys.add(key);
if (!node.objectProperties.has(key)) {
node.objectProperties.set(key, createTypeInfoNode());
}
const propertyNode = node.objectProperties.get(key);
aggregateTypeInfo(value[key], propertyNode, numObjectOccurrences);
}
}
node.objectProperties.forEach((propNode, key) => {
if (!currentKeys.has(key)) {
propNode.parentObjectCount = numObjectOccurrences;
}
});
}
}
const validIdentifierRegex = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
/**
* Recursively generates a TypeScript type string from a TypeInfoNode.
* Uses string literal types if the number of distinct strings is below MAX_STRING_LITERALS.
* @returns The generated TypeScript type string.
*/
function generateTypeString(node, indentation = '', maxKeysPerObject = 25) {
const types = [];
node.primitiveTypes.forEach(type => {
if (type !== 'string') {
// Process string separately
types.push(type);
}
});
if (node.hasStringType) {
// Check if we should use literals (size > 0 and within limit)
if (node.stringLiterals.size > 0 && node.stringLiterals.size < MAX_STRING_LITERALS) {
// Add quoted literals
node.stringLiterals.forEach(literal => {
// Basic escaping for quotes inside the string literal itself
const escapedLiteral = literal.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
types.push(`"${escapedLiteral}"`);
});
}
else {
// Fallback to generic 'string' if too many literals or none were stored
types.push('string');
}
}
if (node.isObject &&
node.objectProperties.size > 0 &&
node.objectProperties.size < maxKeysPerObject) {
const propertyLines = [];
const nextIndentation = indentation + ' ';
const sortedKeys = Array.from(node.objectProperties.keys()).sort();
for (const key of sortedKeys) {
const propertyNode = node.objectProperties.get(key);
const isOptional = propertyNode.parentObjectCount > 0 &&
propertyNode.presenceCount < propertyNode.parentObjectCount;
const formattedKey = validIdentifierRegex.test(key) ? key : `"${key}"`;
const optionalMarker = isOptional ? '?' : '';
const propertyTypeString = generateTypeString(propertyNode, nextIndentation, maxKeysPerObject); // Recursive call
propertyLines.push(`${nextIndentation}${formattedKey}${optionalMarker}: ${propertyTypeString};`);
}
if (propertyLines.length > 0) {
types.push(`{\n${propertyLines.join('\n')}\n${indentation}}`);
}
else {
types.push('Record<string, any>');
}
}
else if (node.isObject) {
types.push('Record<string, any>');
}
if (node.isArray) {
if (node.arrayElementInfo &&
(node.arrayElementInfo.presenceCount > 0 ||
node.arrayElementInfo.primitiveTypes.size > 0 ||
node.arrayElementInfo.stringLiterals.size > 0 ||
node.arrayElementInfo.isObject ||
node.arrayElementInfo.isArray)) {
const elementTypeString = generateTypeString(node.arrayElementInfo, indentation); // Recursive call
// Add parentheses if element type is complex
if (elementTypeString.includes('|') ||
elementTypeString.startsWith('{') ||
elementTypeString.includes('&')) {
types.push(`(${elementTypeString})[]`);
}
else {
types.push(`${elementTypeString}[]`);
}
}
else {
types.push('any[]');
}
}
if (types.length === 0) {
return 'any';
}
else if (types.length === 1) {
return types[0];
}
else {
return types.sort().join(' | ');
}
}
/**
* Generates TypeScript global variable declarations (`declare var name: type;`)
* for each top-level property found in the input objects that is a valid identifier.
* The type represents the deep, combined structure of that property's value, potentially
* using string literal types for strings with few distinct values.
* Keys that are not valid JavaScript identifiers are skipped.
*
* @param objects - The array of input objects.
* @returns A string containing all the generated `declare var` statements, separated by newlines.
* @throws {TypeError} If the input is not an array.
*/
export function generateGlobalVarDeclarations(objects, maxKeysPerObject) {
if (!Array.isArray(objects)) {
throw new TypeError('Input must be an array.');
}
if (objects.length === 0) {
return '';
}
const topLevelPropertyNodes = new Map();
let validObjectCount = 0;
for (const obj of objects) {
if (typeof obj === 'object' && obj !== null && !Array.isArray(obj)) {
validObjectCount++;
for (const key in obj) {
if (Object.prototype.hasOwnProperty.call(obj, key)) {
if (!topLevelPropertyNodes.has(key)) {
topLevelPropertyNodes.set(key, createTypeInfoNode());
}
const propertyRootNode = topLevelPropertyNodes.get(key);
aggregateTypeInfo(obj[key], propertyRootNode, 1);
}
}
}
}
if (validObjectCount === 0) {
return '';
}
const declarations = [];
const sortedKeys = Array.from(topLevelPropertyNodes.keys()).sort();
for (const key of sortedKeys) {
if (validIdentifierRegex.test(key)) {
const propertyNode = topLevelPropertyNodes.get(key);
propertyNode.parentObjectCount = propertyNode.presenceCount; // Set for type generation
const typeString = generateTypeString(propertyNode, undefined, maxKeysPerObject);
if (typeString !== 'any' || propertyNode.presenceCount > 0) {
declarations.push(`declare var ${key}: ${typeString};`);
}
}
}
return declarations.join('\n\n');
}