UNPKG

@spotinst/spinnaker-deck

Version:

Spinnaker-Deck service, forked with support to Spotinst

274 lines (242 loc) 11 kB
'use strict'; // @ts-check const CSS_IMPORT = /\.(css|less|scss|sass)$/; const MODULE_PATH_REGEX = /[./]*(.*)$/; const SPINNAKER_MODULE_PREFIX = /^(core|docker|amazon|titus|google|kubernetes|ecs|huaweicloud|appengine|oracle|cloudfoundry|azure|tencentcloud)\/.*/; /** * Function supplied to an array's sort method that works on the path and module name skipping ., .. and / */ const relativeModuleSort = (a, b) => { const aSource = MODULE_PATH_REGEX.exec(a.source.value)[1]; const bSource = MODULE_PATH_REGEX.exec(b.source.value)[1]; return aSource > bSource ? 1 : aSource === bSource ? 0 : -1; }; /** * Partitions the import specifiers in the given import declaration node based on the import type. * i.e ImportDefaultSpecifier, ImportNamespaceSpecifier and ImportSpecifier * * For example, `import * as React, {useState, useCallback} from 'react';` the following will be returned * { * namespaceSpecifier: <node for `* as React`> , * importSpecifier: [<node for `useState`>, <node for `useCallback`>] * } */ const partitionImportSpecifiers = (importDeclaration) => { if (importDeclaration.specifiers.length === 0) { return { defaultSpecifier: null, namespaceSpecifier: null, importSpecifiers: null, }; } const defaultSpecifier = importDeclaration.specifiers.filter( (specifier) => specifier.type === 'ImportDefaultSpecifier', )[0]; const namespaceSpecifier = importDeclaration.specifiers.filter( (specifier) => specifier.type === 'ImportNamespaceSpecifier', )[0]; const importSpecifiers = importDeclaration.specifiers.filter((specifier) => specifier.type === 'ImportSpecifier'); return { defaultSpecifier, namespaceSpecifier, importSpecifiers }; }; /** * Sorts the import specifiers for the given import declaration node in the following order. * 1. Default Specifier (i.e `React` in `import React, {useState} from 'react'`) * 2. Namespace Specifier (i.e `* as React` in `import * as React, {useState} from 'react'`) * 3. Import Specifier (i.e `useState, useCallback` in `import * as React, {useState, useCallback} from 'react'`) */ const sortImportSpecifiers = (importDeclaration) => { const { defaultSpecifier, namespaceSpecifier, importSpecifiers } = partitionImportSpecifiers(importDeclaration); if (!importSpecifiers) { return importDeclaration; } importSpecifiers.sort((a, b) => a.imported.name.localeCompare(b.imported.name)); importDeclaration.specifiers = [defaultSpecifier, namespaceSpecifier, ...importSpecifiers].filter( (specifier) => !!specifier, ); return importDeclaration; }; /** * Returns the text representation of the given import specifier node. */ const printImportSpecifier = (importSpecifier) => { switch (importSpecifier.type) { case 'ImportDefaultSpecifier': return importSpecifier.local.name; case 'ImportNamespaceSpecifier': return `* as ${importSpecifier.local.name}`; case 'ImportSpecifier': return importSpecifier.local.name !== importSpecifier.imported.name ? `${importSpecifier.imported.name} as ${importSpecifier.local.name}` : importSpecifier.imported.name; } }; /** * Returns the text representation of the given import declaration node. * * NOTE: The built-in `context.getSourceCode().getText(node)` can also return the text representation of a node, but it * preserves the original positions of the import specifiers within the import declaration. */ const printImportDeclaration = (context, importDeclaration) => { const source = importDeclaration.source.value; const partitionedImportSpecifiers = partitionImportSpecifiers(importDeclaration); const importSpecifiersText = Object.entries(partitionedImportSpecifiers).reduce( (importSpecifiersText, [type, importSpecifiers]) => { if (importSpecifiers == null || importSpecifiers.length === 0) { return importSpecifiersText; } importSpecifiersText = importSpecifiersText !== '' ? `${importSpecifiersText}, ` : importSpecifiersText; if (type === 'importSpecifiers') { // There could be more than one specifier for type `ImportSpecifier`, so join them together. const combinedImportSpecifiersText = importSpecifiers.map(printImportSpecifier).join(', '); importSpecifiersText = `${importSpecifiersText}{ ${combinedImportSpecifiersText} }`; } else { // There could only be one import specifier for other types i.e `ImportDefaultSpecifier` and // `ImportNamespaceSpecifier`. importSpecifiersText = `${importSpecifiersText}${printImportSpecifier(importSpecifiers)}`; } return importSpecifiersText; }, '', ); // Try to preserve the preceding comments for each import declaration. const sourceCode = context.getSourceCode(); const isFirstImport = source === sourceCode.ast.body.filter((s) => s.type === 'ImportDeclaration')[0].source.value; let prefix = ''; if (!isFirstImport) { // Don't re-write the preceding comments of the first node since they may not be specific to the import statement. const comments = sourceCode .getCommentsBefore(importDeclaration) .map((comment) => sourceCode.getText(comment)) .join('\n'); prefix = comments ? `${comments}\n` : ''; } return importSpecifiersText !== '' ? `${prefix}import ${importSpecifiersText} from '${source}';` : `${prefix}import '${source}';`; }; /** * Returns a custom textual representation of import declarations which will be used to verify if they are already * sorted. For example * * `import React, {useState, useCallback} from 'react';\nimport angular from 'angular';` * will be written as * `react: React, useState, useCallback\nangular: angular` */ const getText = (importDeclarations) => { return importDeclarations.reduce((output, importDeclaration) => { const specifiersText = (importDeclaration.specifiers || []).map((s) => s.local.name).join(','); return `${output}\n${importDeclaration.source.value}: ${specifiersText}`; }, ''); }; /** * Returns all non `ImportDeclaration` nodes that appear between the first and last `ImportDeclaration` nodes. */ const getAllNonImportDeclarationNodes = (body) => { const importDeclarations = body.filter((node) => node.type === 'ImportDeclaration'); const startIndex = body.findIndex((node) => node.type === 'ImportDeclaration'); const lastIndex = body.findIndex((node) => node == importDeclarations[importDeclarations.length - 1]); const nonImportDeclarationNodes = []; for (let i = startIndex; i <= lastIndex; i++) { if (body[i].type !== 'ImportDeclaration') { nonImportDeclarationNodes.push(body[i]); } } return nonImportDeclarationNodes; }; /** * @type {RuleModule} * * Ensures the import declarations (along with their import specifiers) are sorted based on the following category * and alphabetically within each category. * 1. import npm package * 2. import @spinnaker package * 3. import modules using relative path * 4. import css modules * * NOTE: `ImportDeclaration` refers to the entire import statement. i.e `import foo from './foo';`. `ImportSpecifier` * refers to the members that are imported from the module. i.e `foo` in `import foo from './foo';` */ module.exports = { create(context) { return { Program(program) { const importDeclarations = program.body.filter((node) => node.type === 'ImportDeclaration'); if (!importDeclarations.length) { return; } // Nodes between first and last `ImportDeclarationNodes` that aren't of type `ImportDeclaration`. These nodes // must be re-written at the end of the import declarations. const nonImportDeclarationNodes = getAllNonImportDeclarationNodes(program.body); const start = importDeclarations[0].range[0]; const end = importDeclarations[importDeclarations.length - 1].range[1]; const originalTextOfImportDeclarations = getText(importDeclarations); // Partition the import declarations into four groups `package`, `spinnaker`, `relativeModule`, `css` so that // import declarations within each partition can be sorted alphabetically and written back in the expected // partition order. const partitions = importDeclarations.reduce( (partitions, declarationNode) => { if (CSS_IMPORT.test(declarationNode.source.value)) { partitions.css.push(declarationNode); } else if (declarationNode.source.value.startsWith('@spinnaker')) { partitions.spinnaker.push(declarationNode); } else if (declarationNode.source.value.startsWith('.')) { partitions.relativeModule.push(declarationNode); } else if (SPINNAKER_MODULE_PREFIX.test(declarationNode.source.value)) { partitions.spinnaker.push(declarationNode); } else { partitions.package.push(declarationNode); } return partitions; }, { package: [], spinnaker: [], relativeModule: [], css: [], }, ); const sortedImportedDeclarations = Object.values(partitions).map((importDeclarations) => importDeclarations // Sort import specifiers within each import declaration .map(sortImportSpecifiers) // Now sort all import declarations alphabetically within each partition .sort(relativeModuleSort), ); const sortedTextOfImportDeclarations = getText(sortedImportedDeclarations.flat()); if (originalTextOfImportDeclarations === sortedTextOfImportDeclarations) { return; } const importDeclarationsText = sortedImportedDeclarations .filter((declarationList) => declarationList.length > 0) .map((importDeclarations) => // Print the code from sorted import declarations for each partition importDeclarations .map((importDeclaration) => printImportDeclaration(context, importDeclaration)) .join('\n'), ) // Combine sorted declarations from each partition .join('\n\n'); const sourceCode = context.getSourceCode(); const nonImportDeclarationsText = nonImportDeclarationNodes.length > 0 ? nonImportDeclarationNodes.map((node) => sourceCode.getText(node)).join('\n') : null; const fixedText = nonImportDeclarationsText ? `${importDeclarationsText}\n\n${nonImportDeclarationsText}` : importDeclarationsText; context.report({ fix: (fixer) => fixer.replaceTextRange([start, end], fixedText), message: 'Sort the import statements', node: importDeclarations[0], }); }, }; }, meta: { fixable: 'code', type: 'problem', docs: { description: 'Sort the import statements', }, }, };