Using NX to generate a file based on another file

You can use nx-plugins to generate a file based on another file.
For example, if you want to take all the id’s or attributes from an HTML document (of component) to set aside in a selector file.

Let’s create the generator using NX to generate the needed files.

nx generate @nx/plugin:generator selector-generator

schema.json

{
  "$schema": "http://json-schema.org/schema",
  "$id": "selector",
  "title": "Create selector file based on HTML file",
  "type": "object",
  "properties": {
    "htmlFilePath": {
      "type": "string",
      "description": "The full path to the HTML file",
      "$default": {
        "$source": "argv",
        "index": 0
      },
      "x-prompt": "What is the full path to the HTML file (usually your component.ts or component.html)?"
    },
    "outputDirectory": {
      "type": "string",
      "description": "The directory where the selector file will be created",
      "$default": {
        "$source": "argv",
        "index": 1
      },
      "x-prompt": "In which directory should the selector file be created?"
    }
  },
  "required": ["htmlFilePath"]
}

schema.d.ts

export interface SelectorGeneratorGeneratorSchema {
  htmlFilePath: string;
  outputDirectory?: string;
}

generator.ts (note that you have to change the attributeName or you’ll get an empty file)

import type { Tree } from '@nx/devkit';
import { getWorkspaceLayout } from '@nx/devkit';
import * as path from 'path';
import { join } from 'path';

import type { SelectorGeneratorGeneratorSchema } from './schema';

/**
 * Generates a selectorFileContent based on another file
 * @param tree The Nx Tree (added by default)
 * @param schema The generator options
 * @example  pnpm nx g @yourProject/nx-plugin:selector-generator or yarn nx g @yourProject/nx-plugin:selector-generator
 */
export default async function generateSelectorFile(tree: Tree, schema: SelectorGeneratorGeneratorSchema): Promise<void> {
  if (!tree.exists(schema.htmlFilePath)) {
    throw new Error(`File '${schema.htmlFilePath}' does not exist.`);
  }

  const content = tree.read(schema.htmlFilePath, 'utf-8') as string;

  const selectorValue = extractHostSgQaIdValue(content);
  const attributeValues = extractAttributeValues(content);

  const attributeContent = createSelectors(selectorValue, attributeValues);

  const selectorFileContent = addImports(attributeContent) + attributeContent;

  writeSelectorFileToDisk (tree, schema, selectorFileContent);
}

const extractHostSgQaIdValue = (content: string): string | null => {
  const qaIdRegex = /'qa-id':\s*'([^']+)'/;
  const match = content.match(sgQaIdRegex);

  return match ? match[1] : null;
};

const extractAttributeValues = (content: string, attributeName = 'data-cy'): string[] => {
  const attributeRegex = new RegExp(`${attributeName}="([^"]+)"`, 'g');
  const matches = content.match(attributeRegex);

  if (matches) {
    return matches.map((match) => match.slice(10, -1));
  }

  return [];
};

const hyphenToPascal = (input: string): string => input.replace(/(?:^|-)([a-z])/g, (_match, group1) => group1.toUpperCase());

const createSelectors = (componentSelector: string | null, selectors: string[]): string => {
  let selectorFileContent = '';

  selectors.forEach((selector) => {
    const variableName = `get${hyphenToPascal(selector)}`;

    selectorFileContent += `export const ${variableName} = cy.get('[data-cy="${selector}"]'); \n`;
  });

  return selectorFileContent;
};

/**
 * Creates a new file with the specified content
 */
const writeSelectorFileToDisk = (tree: Tree, schema: SelectorGeneratorGeneratorSchema, selectorFileContent: string): void => {
  const fileName = path
    .basename(schema.htmlFilePath)
    .replace(/([a-z])([A-Z])/g, '$1-$2')
    .toLowerCase();

  const outputDir = schema.outputDirectory ?? getWorkspaceLayout(tree).libsDir;
  const outputFilePath = join(`${outputDir}`, `${fileName}.selectors.ts`).replace('.component.ts', '');

  tree.write(outputFilePath, selectorFileContent);
};

generator.spec

import type { Tree } from '@nx/devkit';
import { createTreeWithEmptyWorkspace } from '@nx/devkit/testing';
import { join } from 'path';
import generateSelectorFile from './generator';
import type { SelectorGeneratorGeneratorSchema } from './schema';

describe('Selector file generator', () => {
  let libTree: Tree;

  beforeEach(() => {
    libTree = createTreeWithEmptyWorkspace({ layout: 'apps-libs' });
    libTree.write(
      'libs/abc/test/feature-manage/src/lib/test-form/test-form.component.ts',
      `
  template: \`
        <form [formGroup]="form">
          <mat-form-field data-cy="some-form-field">
            <input data-cy="some-input" />
          </mat-form-field>
        </form>
      })
      \`
      export class TestFormComponent { }`
    );
  });

  it('should generate the selector file', async () => {
    const options = {
      htmlFilePath: 'libs/abc/test/feature-manage/src/lib/test-form/test-form.component.ts',
      outputDirectory: 'apps/abc/app-e2e/src/support/test',
    } satisfies SelectorGeneratorGeneratorSchema;

    await generateSelectorFile(libTree, options);

    // Read the generated file's content
    const fileContents = libTree.read(join(options.outputDirectory, 'test-form.selectors.ts'), 'utf-8');

    // Check if the file is generated and has content
    expect(fileContents).toContain(`export const getTestFormComponent = cy.get('[data-cy="some-form-field"]');`);
    expect(fileContents).toContain(`export const getTestFormComponent = cy.get('[data-cy="some-input"]');`);
  });
});

You can now run

pnpm nx g @yourProject/nx-plugin:selector-generator 
or
yarn nx g @yourProject/nx-plugin:selector-generator