inventoresed
Version:
Z-Wave driver written entirely in JavaScript/TypeScript
210 lines (190 loc) • 6.03 kB
text/typescript
/*!
* This script generates the exports for all utility types from `src/lib/commandclass/*CC.ts`
*/
import {
formatWithPrettier,
hasComment,
loadTSConfig,
projectRoot,
} from "@zwave-js/maintenance";
import { compareStrings } from "@zwave-js/shared";
import * as fs from "fs-extra";
import * as path from "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)
.replace(/\\/g, "/");
// 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
.replace(/\\/g, "/")
// TS imports may not end with ".ts"
.replace(/\.ts$/, "")
// 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.pathExists(ccIndexFile))
? await fs.readFile(ccIndexFile, "utf8")
: "";
fileContent = formatWithPrettier(ccIndexFile, fileContent);
if (fileContent !== originalFileContent) {
console.log("CC index file changed");
await fs.writeFile(ccIndexFile, fileContent, "utf8");
}
}
if (require.main === module) void generateCCExports();