217 lines
5.9 KiB
TypeScript
217 lines
5.9 KiB
TypeScript
/*!
|
|
* This script generates the exports for all utility types from `src/lib/commandclass/*CC.ts`
|
|
*/
|
|
|
|
import {
|
|
formatWithDprint,
|
|
hasComment,
|
|
loadTSConfig,
|
|
projectRoot,
|
|
} from "@zwave-js/maintenance";
|
|
import { compareStrings } from "@zwave-js/shared";
|
|
import esMain from "es-main";
|
|
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
import ts from "typescript";
|
|
|
|
// Define where the CC index file is located
|
|
const ccIndexFile = path.join(projectRoot, "src/cc/index.ts");
|
|
|
|
function hasPublicAPIComment(
|
|
node: ts.Node,
|
|
sourceFile: ts.SourceFile,
|
|
): boolean {
|
|
return hasComment(sourceFile, node, (text) => text.includes("@publicAPI"));
|
|
}
|
|
|
|
function findExports() {
|
|
// Create a Program to represent the project, then pull out the
|
|
// source file to parse its AST.
|
|
|
|
const tsConfig = loadTSConfig("cc");
|
|
const program = ts.createProgram(tsConfig.fileNames, tsConfig.options);
|
|
const checker = program.getTypeChecker();
|
|
|
|
// Used to remember the exports we found
|
|
const ccExports = new Map<string, { name: string; typeOnly: boolean }[]>();
|
|
function addExport(
|
|
filename: string,
|
|
name: string,
|
|
typeOnly: boolean,
|
|
): void {
|
|
if (!ccExports.has(filename)) ccExports.set(filename, []);
|
|
ccExports.get(filename)!.push({ name, typeOnly });
|
|
}
|
|
|
|
function inheritsFromCommandClass(node: ts.ClassDeclaration): boolean {
|
|
let type: ts.InterfaceType | undefined = checker.getTypeAtLocation(
|
|
node,
|
|
) as ts.InterfaceType;
|
|
while (type) {
|
|
if (type.symbol.name === "CommandClass") return true;
|
|
type = checker.getBaseTypes(type)[0] as
|
|
| ts.InterfaceType
|
|
| undefined;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
// Scan all source files
|
|
for (const sourceFile of program.getSourceFiles()) {
|
|
const relativePath = path
|
|
.relative(projectRoot, sourceFile.fileName)
|
|
.replaceAll("\\", "/");
|
|
|
|
// Only look at files in this package
|
|
if (relativePath.startsWith("..")) continue;
|
|
|
|
// Only look at the cc dir
|
|
if (!relativePath.includes("src/cc/")) {
|
|
continue;
|
|
}
|
|
// Ignore test files and the index
|
|
if (
|
|
relativePath.endsWith(".test.ts")
|
|
|| relativePath.endsWith("index.ts")
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
// Visit each root node to see if it has a `@publicAPI` comment
|
|
ts.forEachChild(sourceFile, (node) => {
|
|
// Define which declaration types we need to export
|
|
if (
|
|
ts.isEnumDeclaration(node)
|
|
|| ts.isTypeAliasDeclaration(node)
|
|
|| ts.isInterfaceDeclaration(node)
|
|
|| ts.isClassDeclaration(node)
|
|
|| ts.isFunctionDeclaration(node)
|
|
|| ts.isArrowFunction(node)
|
|
) {
|
|
if (!node.name) return;
|
|
|
|
// Export all CommandClass implementations
|
|
if (
|
|
ts.isClassDeclaration(node)
|
|
&& node.name.text.includes("CC")
|
|
&& inheritsFromCommandClass(node)
|
|
) {
|
|
addExport(sourceFile.fileName, node.name.text, false);
|
|
return;
|
|
}
|
|
|
|
if (!hasPublicAPIComment(node, sourceFile)) return;
|
|
|
|
// Make sure we're trying to access a node that is actually exported
|
|
if (
|
|
!node.modifiers?.some(
|
|
(m) => m.kind === ts.SyntaxKind.ExportKeyword,
|
|
)
|
|
) {
|
|
const location = ts.getLineAndCharacterOfPosition(
|
|
sourceFile,
|
|
node.getStart(sourceFile, false),
|
|
);
|
|
throw new Error(
|
|
`${relativePath}:${location.line} Found @publicAPI comment, but the node ${node.name.text} is not exported!`,
|
|
);
|
|
}
|
|
addExport(
|
|
sourceFile.fileName,
|
|
node.name.text,
|
|
ts.isTypeAliasDeclaration(node)
|
|
|| ts.isInterfaceDeclaration(node),
|
|
);
|
|
} else if (
|
|
ts.isExportDeclaration(node)
|
|
&& hasPublicAPIComment(node, sourceFile)
|
|
&& node.exportClause
|
|
&& ts.isNamedExports(node.exportClause)
|
|
) {
|
|
// Also include all re-exports from other locations in the project
|
|
for (const exportSpecifier of node.exportClause.elements) {
|
|
addExport(
|
|
sourceFile.fileName,
|
|
exportSpecifier.name.text,
|
|
node.isTypeOnly || exportSpecifier.isTypeOnly,
|
|
);
|
|
}
|
|
} else if (
|
|
ts.isVariableStatement(node)
|
|
&& node.modifiers?.some(
|
|
(m) => m.kind === ts.SyntaxKind.ExportKeyword,
|
|
)
|
|
// Export consts marked with @publicAPI
|
|
&& (hasPublicAPIComment(node, sourceFile)
|
|
// and the xyzCCValues const
|
|
|| node.declarationList.declarations.some((d) =>
|
|
d.name.getText().endsWith("CCValues")
|
|
))
|
|
) {
|
|
for (const variable of node.declarationList.declarations) {
|
|
if (ts.isIdentifier(variable.name)) {
|
|
addExport(
|
|
sourceFile.fileName,
|
|
variable.name.text,
|
|
false,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
return ccExports;
|
|
}
|
|
|
|
export async function generateCCExports(): Promise<void> {
|
|
let fileContent = `
|
|
// This file is auto-generated by maintenance/generateCCExports.ts
|
|
// Do not edit it by hand or your changes will be lost!
|
|
|
|
`;
|
|
|
|
// Generate type and value exports for all found symbols
|
|
for (
|
|
const [filename, fileExports] of [...findExports().entries()].sort(
|
|
([fileA], [fileB]) => compareStrings(fileA, fileB),
|
|
)
|
|
) {
|
|
const relativePath = path
|
|
.relative(ccIndexFile, filename)
|
|
// normalize to slashes
|
|
.replaceAll("\\", "/")
|
|
// TS imports may not end with ".ts"
|
|
.replace(/\.ts$/, ".js")
|
|
// By passing the index file as "from", we get an erraneous "../" at the path start
|
|
.replace(/^\.\.\//, "./");
|
|
const typeExports = fileExports.filter((e) => e.typeOnly);
|
|
if (typeExports.length) {
|
|
fileContent += `export type { ${
|
|
typeExports
|
|
.map((e) => e.name)
|
|
.join(", ")
|
|
} } from "${relativePath}"\n`;
|
|
}
|
|
const valueExports = fileExports.filter((e) => !e.typeOnly);
|
|
if (valueExports.length) {
|
|
fileContent += `export { ${
|
|
valueExports
|
|
.map((e) => e.name)
|
|
.join(", ")
|
|
} } from "${relativePath}"\n`;
|
|
}
|
|
}
|
|
|
|
// And write the file if it changed
|
|
const originalFileContent = await fs.readFile(ccIndexFile, "utf8").catch(
|
|
() => undefined,
|
|
);
|
|
fileContent = formatWithDprint(ccIndexFile, fileContent);
|
|
if (fileContent !== originalFileContent) {
|
|
console.log("CC index file changed");
|
|
await fs.writeFile(ccIndexFile, fileContent, "utf8");
|
|
}
|
|
}
|
|
|
|
if (esMain(import.meta)) void generateCCExports();
|