Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: Allow Explicit Separator after Top-of-file-comments #92

Merged
merged 14 commits into from
May 19, 2023
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 28 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ Since then more critical features & fixes have been added, and the options have
- [3. Add spaces between import groups](#3-add-spaces-between-import-groups)
- [4. Group type imports separately from values](#4-group-type-imports-separately-from-values)
- [5. Group aliases with local imports](#5-group-aliases-with-local-imports)
- [6. Enforce a blank line after top of file comments](#6-enforce-a-blank-line-after-top-of-file-comments)
- [`importOrderTypeScriptVersion`](#importordertypescriptversion)
- [`importOrderParserPlugins`](#importorderparserplugins)
- [Prevent imports from being sorted](#prevent-imports-from-being-sorted)
Expand Down Expand Up @@ -231,7 +232,7 @@ import MyApp from './MyApp';
Imports of CSS files are often placed at the bottom of the list of imports, and can be accomplished like so:

```json
"importOrder": ["<THIRD_PARTY_MODULES>", "^(?!.*[.]css$)[./].*$", ".css$",]
"importOrder": ["<THIRD_PARTY_MODULES>", "^(?!.*[.]css$)[./].*$", ".css$"]
```

e.g.:
Expand Down Expand Up @@ -265,7 +266,7 @@ import MyApp from './MyApp';
If you're using Flow or TypeScript, you might want to separate out your type imports from imports of values. And to be especially fancy, you can even group 3rd party types together, and your own local type imports separately:

```json
"importOrder": ["<TYPES>", "<TYPES>^[.]", "<THIRD_PARTY_MODULES>", "^[.]",]
"importOrder": ["<TYPES>", "<TYPES>^[.]", "<THIRD_PARTY_MODULES>", "^[.]"]
```

e.g.:
Expand Down Expand Up @@ -299,6 +300,31 @@ import icon from '@assets/icon';
import App from './App';
```

##### 6. Enforce a blank line after top of file comments

If you have pragma-comments at the top of file, or you have boilerplate copyright announcements, you may be interested in separating that content from your code imports. Explicitly providing `"<BUILTIN_MODULES>", "<THIRD_PARTY_MODULES>"` in your list will allow you to add a separator before them!

```json
"importOrder": [
"", // Include the separator here! And also provide "<BUILTIN_MODULES>", "<THIRD_PARTY_MODULES>" later
"<BUILTIN_MODULES>", // This must be present in the list somewhere
"<THIRD_PARTY_MODULES>", // This must be present in the list somewhere
fbartho marked this conversation as resolved.
Show resolved Hide resolved
"^[.]"]
```

e.g.:

```ts
/**
* @prettier
*/

import { debounce, reduce } from 'lodash';
fbartho marked this conversation as resolved.
Show resolved Hide resolved
import { Users } from '@api';
import icon from '@assets/icon';
import App from './App';
```

#### `importOrderTypeScriptVersion`

**type**: `string`
Expand Down
46 changes: 8 additions & 38 deletions src/preprocessors/preprocessor.ts
Original file line number Diff line number Diff line change
@@ -1,58 +1,28 @@
import { parse as babelParser, ParserOptions } from '@babel/parser';
import traverse, { NodePath } from '@babel/traverse';
import { ImportDeclaration, isTSModuleDeclaration } from '@babel/types';
import semver from 'semver';

import { TYPES_SPECIAL_WORD } from '../constants';
import { PrettierOptions } from '../types';
import { getCodeFromAst } from '../utils/get-code-from-ast';
import { getExperimentalParserPlugins } from '../utils/get-experimental-parser-plugins';
import { getSortedNodes } from '../utils/get-sorted-nodes';
import { examineAndNormalizePluginOptions } from '../utils/normalize-plugin-options';

export function preprocessor(code: string, options: PrettierOptions): string {
const { importOrderParserPlugins, importOrder, filepath } = options;
let { importOrderTypeScriptVersion } = options;
const isTSSemverValid = semver.valid(importOrderTypeScriptVersion);
const { plugins, ...remainingOptions } =
examineAndNormalizePluginOptions(options);

if (!isTSSemverValid) {
console.warn(
`[@ianvs/prettier-plugin-sort-imports]: The option importOrderTypeScriptVersion is not a valid semver version and will be ignored.`,
);
importOrderTypeScriptVersion = '1.0.0';
}

// Do not combine type and value imports if `<TYPES>` is specified explicitly
let importOrderCombineTypeAndValueImports = importOrder.some((group) =>
group.includes(TYPES_SPECIAL_WORD),
)
? false
: true;

const allOriginalImportNodes: ImportDeclaration[] = [];
let plugins = getExperimentalParserPlugins(importOrderParserPlugins);
// Do not inject jsx plugin for non-jsx ts files
if (filepath.endsWith('.ts')) {
plugins = plugins.filter((p) => p !== 'jsx');
}
const parserOptions: ParserOptions = {
sourceType: 'module',
attachComment: true,
plugins,
};

// Disable importOrderCombineTypeAndValueImports if typescript is not set to a version that supports it
if (
parserOptions.plugins?.includes('typescript') &&
semver.lt(importOrderTypeScriptVersion, '4.5.0')
) {
importOrderCombineTypeAndValueImports = false;
}

const ast = babelParser(code, parserOptions);

const directives = ast.program.directives;
const interpreter = ast.program.interpreter;

const allOriginalImportNodes: ImportDeclaration[] = [];
traverse(ast, {
ImportDeclaration(path: NodePath<ImportDeclaration>) {
const tsModuleParent = path.findParent((p) =>
Expand All @@ -69,10 +39,10 @@ export function preprocessor(code: string, options: PrettierOptions): string {
return code;
}

const nodesToOutput = getSortedNodes(allOriginalImportNodes, {
importOrder,
importOrderCombineTypeAndValueImports,
});
const nodesToOutput = getSortedNodes(
allOriginalImportNodes,
remainingOptions,
);

return getCodeFromAst({
nodesToOutput,
Expand Down
22 changes: 21 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { ParserPlugin } from '@babel/parser';
import {
type EmptyStatement,
type ExpressionStatement,
Expand Down Expand Up @@ -46,13 +47,28 @@ export type SomeSpecifier =
| ImportNamespaceSpecifier;
export type ImportRelated = ImportOrLine | SomeSpecifier;

export interface InspectedAndNormalizedOptions {
IanVS marked this conversation as resolved.
Show resolved Hide resolved
importOrder: PrettierOptions['importOrder'];
importOrderCombineTypeAndValueImports: boolean;
hasAnyCustomGroupSeparatorsInImportOrder: boolean;
provideGapAfterTopOfFileComments: boolean;
plugins: ParserPlugin[];
}

export type GetSortedNodes = (
nodes: ImportDeclaration[],
options: Pick<PrettierOptions, 'importOrder'> & {
options: Pick<InspectedAndNormalizedOptions, 'importOrder'> & {
importOrderCombineTypeAndValueImports: boolean;
hasAnyCustomGroupSeparatorsInImportOrder?: boolean;
provideGapAfterTopOfFileComments?: boolean;
},
) => ImportOrLine[];

export type GetSortedNodesByImportOrder = (
nodes: ImportDeclaration[],
options: Pick<InspectedAndNormalizedOptions, 'importOrder'>,
) => ImportOrLine[];

export type GetChunkTypeOfNode = (node: ImportDeclaration) => ChunkType;

export type GetImportFlavorOfNode = (node: ImportDeclaration) => FlavorType;
Expand All @@ -65,3 +81,7 @@ export type MergeNodesWithMatchingImportFlavors = (
export type ExplodeTypeAndValueSpecifiers = (
nodes: ImportDeclaration[],
) => ImportDeclaration[];

export interface CommentAttachmentOptions {
provideGapAfterTopOfFileComments?: boolean;
}
28 changes: 23 additions & 5 deletions src/utils/__tests__/adjust-comments-on-sorted-nodes.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expect, test } from 'vitest';

import type { ImportOrLine } from '../../types';
import type { CommentAttachmentOptions, ImportOrLine } from '../../types';
import { adjustCommentsOnSortedNodes } from '../adjust-comments-on-sorted-nodes';
import { getImportNodes } from '../get-import-nodes';

Expand All @@ -12,6 +12,8 @@ function trailingComments(node: ImportOrLine): string[] {
return node.trailingComments?.map((c) => c.value) ?? [];
}

const defaultAttachmentOptions: CommentAttachmentOptions = {};

test('it preserves the single leading comment for each import declaration', () => {
const importNodes = getImportNodes(`
import {x} from "c";
Expand All @@ -22,7 +24,11 @@ test('it preserves the single leading comment for each import declaration', () =
`);
expect(importNodes).toHaveLength(3);
const finalNodes = [importNodes[2], importNodes[1], importNodes[0]];
const adjustedNodes = adjustCommentsOnSortedNodes(importNodes, finalNodes);
const adjustedNodes = adjustCommentsOnSortedNodes(
importNodes,
finalNodes,
defaultAttachmentOptions,
);
expect(adjustedNodes).toHaveLength(3);
expect(leadingComments(adjustedNodes[0])).toEqual([' comment a']);
expect(trailingComments(adjustedNodes[0])).toEqual([]);
Expand All @@ -47,7 +53,11 @@ test('it preserves multiple leading comments for each import declaration', () =>
`);
expect(importNodes).toHaveLength(3);
const finalNodes = [importNodes[2], importNodes[1], importNodes[0]];
const adjustedNodes = adjustCommentsOnSortedNodes(importNodes, finalNodes);
const adjustedNodes = adjustCommentsOnSortedNodes(
importNodes,
finalNodes,
defaultAttachmentOptions,
);
expect(adjustedNodes).toHaveLength(3);
expect(leadingComments(adjustedNodes[0])).toEqual([
' comment a1',
Expand Down Expand Up @@ -75,7 +85,11 @@ test('it does not move comments more than one line before all import declaration
`);
expect(importNodes).toHaveLength(3);
const finalNodes = [importNodes[2], importNodes[1], importNodes[0]];
const adjustedNodes = adjustCommentsOnSortedNodes(importNodes, finalNodes);
const adjustedNodes = adjustCommentsOnSortedNodes(
importNodes,
finalNodes,
defaultAttachmentOptions,
);
expect(adjustedNodes).toHaveLength(4);
// Comment c1 is above the first import, so it stays with the top-of-file attached to a dummy statement
expect(adjustedNodes[0].type).toEqual('EmptyStatement');
Expand All @@ -99,7 +113,11 @@ test('it does not affect comments after all import declarations', () => {
`);
expect(importNodes).toHaveLength(3);
const finalNodes = [importNodes[2], importNodes[1], importNodes[0]];
const adjustedNodes = adjustCommentsOnSortedNodes(importNodes, finalNodes);
const adjustedNodes = adjustCommentsOnSortedNodes(
importNodes,
finalNodes,
defaultAttachmentOptions,
);
expect(adjustedNodes).toHaveLength(3);
expect(leadingComments(adjustedNodes[0])).toEqual([]);
expect(trailingComments(adjustedNodes[0])).toEqual([]);
Expand Down
3 changes: 2 additions & 1 deletion src/utils/__tests__/get-all-comments-from-nodes.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ import { expect, test } from 'vitest';
import { getAllCommentsFromNodes } from '../get-all-comments-from-nodes';
import { getImportNodes } from '../get-import-nodes';
import { getSortedNodes } from '../get-sorted-nodes';
import { testingOnly } from '../normalize-plugin-options';

const getSortedImportNodes = (code: string, options?: ParserOptions) => {
const importNodes: ImportDeclaration[] = getImportNodes(code, options);

return getSortedNodes(importNodes, {
importOrder: [],
importOrder: testingOnly.normalizeImportOrderOption([]),
importOrderCombineTypeAndValueImports: true,
});
};
Expand Down
7 changes: 5 additions & 2 deletions src/utils/__tests__/get-code-from-ast.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ import { expect, test } from 'vitest';
import { getCodeFromAst } from '../get-code-from-ast';
import { getImportNodes } from '../get-import-nodes';
import { getSortedNodes } from '../get-sorted-nodes';
import { testingOnly } from '../normalize-plugin-options';

const emptyImportOrder = testingOnly.normalizeImportOrderOption([]);

test('sorts imports correctly', () => {
const code = `import z from 'z';
Expand All @@ -15,7 +18,7 @@ import a from 'a';
`;
const importNodes = getImportNodes(code);
const sortedNodes = getSortedNodes(importNodes, {
importOrder: [],
importOrder: emptyImportOrder,
importOrderCombineTypeAndValueImports: true,
});
const formatted = getCodeFromAst({
Expand Down Expand Up @@ -47,7 +50,7 @@ import type {See} from 'c';
`;
const importNodes = getImportNodes(code, { plugins: ['typescript'] });
const sortedNodes = getSortedNodes(importNodes, {
importOrder: [],
importOrder: emptyImportOrder,
importOrderCombineTypeAndValueImports: true,
});
const formatted = getCodeFromAst({
Expand Down
4 changes: 2 additions & 2 deletions src/utils/__tests__/get-comment-registry.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
attachCommentsToOutputNodes,
CommentAssociation,
getCommentRegistryFromImportDeclarations,
testingOnlyExports,
testingOnly,
} from '../get-comment-registry';

describe('getCommentRegistryFromImportDeclarations', () => {
Expand Down Expand Up @@ -64,7 +64,7 @@ describe('attachCommentsToOutputNodes', () => {
needsTopOfFileOwner: true,
comment,
ownerIsSpecifier: false,
commentId: testingOnlyExports.nodeId(comment),
commentId: testingOnly.nodeId(comment),
owner: firstImport,
association: CommentAssociation.trailing,
processingPriority: 0,
Expand Down
Loading