canopy icon indicating copy to clipboard operation
canopy copied to clipboard

Deprecations & upcoming breaking changes - v12.0.0

Open pauleustice opened this issue 1 year ago • 1 comments

ℹ️ Small breaking changes are more easily consumed when shipped together in a major version bump.

💡 When identifying such a change, such as removing a CSS var, add a deprecation notice and where possible provide deprecation warnings in the console.

💬 Add a comment to this issue to log current deprecations and upcoming minor breaking changes.

🚀 The pull request should be merged into the next branch, where it can be part of the next pre-release. At an appropriate time, next can be merged into master creating a major release of Canopy (with some nice release notes).

♻️ When released, this issue can be closed down and another one spun up for the next batch.

pauleustice avatar Jun 04 '24 09:06 pauleustice

The upcoming v12 includes an upgrade to Angular 17.

Most importantly, it converts all components and directives to be standalone. This means that there is a significant amount of work involved in upgrading to v12 of Canopy.

This NodeJS script will highlight Canopy components and directives that are used within HTML files but not declared within the relevant spec files. This can help with updating your spec files to import all relevant Canopy imports:

Details
const fs = require('fs');
const path = require('path');
const cheerio = require('cheerio');

/*
NOTE: This script is designed to be run in a Node.js environment
It will search through all HTML files in the specified directory and check if the corresponding spec file exists
It will then check the HTML file for any custom components or directives and check if they are imported in the spec file
If they are not imported, it will output the missing components and directives

As it rather dumbly checks whether a component or directive exists in the Typescript (and not whether it is specified
within the imports: [] array, it can miss out on some if it's imported but not declared in the module
 */

function listFilesRecursively(directory, extension) {
  let fileList = [];

  fs.readdirSync(directory).forEach(file => {
    const absolutePath = path.join(directory, file);

    if (fs.statSync(absolutePath).isDirectory()) {
      fileList = fileList.concat(listFilesRecursively(absolutePath, extension));
    } else {
      if (file.endsWith(extension)) {
        fileList.push(absolutePath);
      }
    }
  });

  return fileList;
}

const searchDir = 'apps/';
const htmlFiles = listFilesRecursively(searchDir, '.component.html');
const specFiles = listFilesRecursively(searchDir, '.spec.ts');

function kebabToCamel(kebab) {
  let words = kebab.split('-');
  return words[0].charAt(0).toUpperCase() + words[0].slice(1) + words.slice(1).map(word => word.charAt(0).toUpperCase() + word.slice(1)).join('');
}

let directivesMissingCount = 0;
let componentsMissingCount = 0;

// The directiveMap object maps the multiple possible html directives to the related import name. These are
// always one to one or one to many. These assignments must be created / updated manually.
// As such, I may have missed a bunch. You could get a list of all directives in your apps by looping over the html files and creating a new Set()

// Additionally, some directive names in the HTML are different to the import name in the TS file
// e.g. <div lgContainer> in the HTML is actually LgGridContainerDirective in the TS file

const directiveMap = {
  LgGridContainerDirective: [ 'lgContainer'],
  LgGridRowDirective: [ 'lgRow' ],
  LgGridColDirective: [ 'lgColMdOffset', 'lgColLgOffset', 'lgColSmOffset', 'lgColSm', 'lgColMd', 'lgColLg', 'lgCol' ],
  LgMarginDirective: [ 'lgMarginTop', 'lgMargin', 'lgMarginBottom', 'lgMarginRight', 'lgMarginVertical', 'lgMarginLeft' ],
  LgPaddingDirective: [ 'lgPaddingBottom', 'lgPadding', 'lgPaddingVertical', 'lgPaddingTop', 'lgPaddingHorizontal', 'lgPaddingLeft' ],
  LgShadowDirective: [ 'lgShadow' ],
  LgTabNavBarLinkDirective: [ 'lgTabNavBarLink' ],
  LgHideAtDirective: [ 'lgHideAt' ],
  LgVariantDirective: [ 'lgVariant' ],
  LgInputDirective: [ 'lgInput' ],
  LgSuffixDirective: [ 'lgSuffix' ],
  LgSortCodeDirective: [ 'lgSortCode' ],
  LgSelectDirective: [ 'lgSelect'],
};

// Some component names in the HTML are different to the component name in the TS file
// e.g. <lg-filter-multiple-group> in the HTML is actually LgCheckboxGroupComponent in the TS file

const componentMap = {
  LgFilterGroupComponent: 'LgRadioGroupComponent',
  LgFilterButtonComponent: 'LgRadioButtonComponent',
  LgFilterMultipleGroupComponent: 'LgCheckboxGroupComponent',
};

// Iterate over each HTML file
htmlFiles.forEach(htmlFile => {
  const htmlContent = fs.readFileSync(htmlFile, 'utf8');
  const $ = cheerio.load(htmlContent, { xmlMode: true });

  const specFile = htmlFile.replace('.html', '.spec.ts');

  if (specFiles.includes(specFile)) {
    const missingDirectives = new Set();
    const missingComponents = new Set();
    const missingComponents2 = new Set();

    const tsContent = fs.readFileSync(specFile, 'utf8');
    const filePathParts = specFile.replace(searchDir, '').split('/');
    const fileNameOnly = filePathParts[filePathParts.length - 1];

    const lgComponents = $('*').filter((i, el) => el.name.startsWith('lg-'));

    // Find any missing directives

    Object.entries(directiveMap).forEach(([ tsName, htmlNames ]) => {
      htmlNames.forEach(htmlName => {
        if (htmlContent.includes(htmlName) && !tsContent.includes(tsName)) {
          missingDirectives.add(tsName);
          directivesMissingCount++;
        }
      });
    });

    // Find any missing components where the element is a component, e.g.
    // <lg-card-component>...</lg-card-component>

    lgComponents.each((i, component) => {
      const kebabCase = `${kebabToCamel(component.name)}Component`;
      const componentImportName = componentMap[kebabCase] ?? kebabCase;

      if (!tsContent.includes(componentImportName)) {
        missingComponents.add(componentImportName);
        componentsMissingCount++;
      }
    });

    // Find any missing components where the element is a directive, e.g.
    // <td lg-table-cell>...</td>

    $('*').each((index, element) => {
      for (let attr in element.attribs) {
        const kebabCase = `${kebabToCamel(attr)}Component`;
        const componentImportName = componentMap[kebabCase] ?? kebabCase;

        if (attr.startsWith('lg-') && !tsContent.includes(componentImportName)) {
          missingComponents.add(componentImportName);
          componentsMissingCount++;
        }
      }
    });

    // Output missing components and directives

    if (missingDirectives.size || missingComponents.size) {
      console.log(`\n\nat (${specFile}:1:1)`);
      console.log('---------------------------------');

      const alreadyMocksComponents = tsContent.includes('MockComponents');
      const alreadyMocksDirectives = tsContent.includes('MockDirectives');

      if (!tsContent.includes('imports: [')) {
        console.log('imports: [');
      }

      const componentOutput = alreadyMocksComponents
        ? [ ...missingComponents ].join(', ')
        :  `MockComponents(${[ ...missingComponents ].join(', ')}),`;

      const directivesOutput = alreadyMocksDirectives
        ? [ ...missingDirectives ].join(', ')
        : `MockDirectives(${[ ...missingDirectives ].join(', ')}),`;

      if (missingDirectives.size) {
        console.log(directivesOutput);
      }

      if (missingComponents.size) {
        console.log(componentOutput);
      }

      if (!tsContent.includes('imports: [')) {
        console.log('],');
      }
    }
  }
});

console.log(`Total Directives Missing: ${directivesMissingCount}`);
console.log(`Total Components Missing: ${componentsMissingCount}`);

pauleustice avatar Jun 04 '24 13:06 pauleustice

Canopy v12 has now been released: https://github.com/Legal-and-General/canopy/releases/tag/v12.0.0

pauleustice avatar Aug 28 '24 10:08 pauleustice