javascriptnode.jsangulartypescript

How to generate dynamic metadata from an Angular component using node js?


I want to create a library with components example pages generated automatically by a node js code. Using some Copilot help, I got this function to read the component's metadata, but it is returning me empty. My knowledge in node is not so developed yet and I want to understand the process, not only copy from AI. This is the function with the interfaces - the code is in typescript for node. The function receives the angular component file path as parameter

interface InputMetadata {
  name: string;
  type: string | null;
  defaultValue: string | null;
}

interface ComponentMetadata {
  selector: string;
  standalone: boolean;
  inputs: InputMetadata[];
}

// Function to read component metadata
function readComponentMetadata(componentPath: string): ComponentMetadata {
  const componentFile = fs.readFileSync(componentPath, 'utf-8');
  const sourceFile = ts.createSourceFile(componentPath, componentFile, ts.ScriptTarget.Latest, true);
  const inputs: InputMetadata[] = [];
  let selector: string = '';
  let standalone: boolean = false;

  function visit(node: ts.Node) {
    if (ts.isClassDeclaration(node) && node.decorators) {
      node.decorators.forEach(decorator => {
        if (ts.isCallExpression(decorator.expression) && decorator.expression.expression.getText() === 'Component') {
          const args = decorator.expression.arguments;
          if (args.length) {
            const componentMetadata = args[0] as ts.ObjectLiteralExpression;
            componentMetadata.properties.forEach(property => {
              if (ts.isPropertyAssignment(property)) {
                if (property.name.getText() === 'selector') {
                  selector = (property.initializer as ts.StringLiteral).text;
                } else if (property.name.getText() === 'standalone') {
                  standalone = (property.initializer.kind === ts.SyntaxKind.TrueKeyword);
                }
              }
            });
          }
        }
      });
    }
    if (ts.isPropertyDeclaration(node) && node.decorators) {
      node.decorators.forEach(decorator => {
        if (ts.isCallExpression(decorator.expression) && decorator.expression.expression.getText() === 'Input') {
          const name = node.name.getText();
          const type = node.type ? node.type.getText() : null;
          const initializer = node.initializer ? node.initializer.getText() : null;
          inputs.push({name, type, defaultValue: initializer});
        }
      });
    }
    ts.forEachChild(node, visit);
  }

  visit(sourceFile);
  return {selector, standalone, inputs};
}

The file must be "compiled" using npx tsc and then the js file can be called.

The node and typescript I'm using are:

"devDependencies": {
  "@types/node": "^22.10.1",
  "typescript": "^5.7.2"
}

This is the tsconfig.json file:

{
  "compilerOptions": {
    "target": "ES6",
    "module": "commonjs",
    "outDir": "./dist",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*.ts"],
  "exclude": ["node_modules"]
}

Solution

  • For me the code was throwing type errors, like mentioned in the comments. Seems like a ClassDeclaration does not have a decorators property. To get the decorators, you need to call ts.getDecorators(node). Updated code:

    function readComponentMetadata(componentPath: string): ComponentMetadata {
      // nothing changed here
    
      function visit(node: ts.Node) {
        if (ts.isClassDeclaration(node)) {
          const decorators = ts.getDecorators(node);
          decorators?.forEach(decorator => {
            // nothing changed here
          });
        }
        if (ts.isPropertyDeclaration(node)) {
          const decorators = ts.getDecorators(node);
          decorators?.forEach(decorator => {
            // nothing changed here
          });
        }
        ts.forEachChild(node, visit);
      }
    
      visit(sourceFile);
      return {selector, standalone, inputs};
    }
    

    I have also tested this code in a real Angular project, seems to work fine.