Skip to content

Commit

Permalink
Feature: Allow Explicit Separator after Top-of-file-comments (#92)
Browse files Browse the repository at this point in the history
Primarily, this makes it possible for people to declare that there
should be a gap after top-of-file-comments (before other imports).

Additionally:
- Centralizes plugin options parsing so it only happens once
- I think it fixes a regression that was introduced in `4.0.0-alpha.?`
around `tests/ImportsSeparatedIntoSideEffectGroups`
- Cleans up some more trailing commas in the readme-json-options

---------

Co-authored-by: Ian VanSchooten <ian.vanschooten@gmail.com>
  • Loading branch information
fbartho and IanVS committed May 19, 2023
1 parent 34642cc commit bfda0f5
Show file tree
Hide file tree
Showing 35 changed files with 654 additions and 151 deletions.
29 changes: 27 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,30 @@ 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, you can add that separator first.

```json
"importOrder": [
"",
"^[.]"
]
```

e.g.:

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

import { promises } from 'fs';
import { Users } from '@api';
import icon from '@assets/icon';
import App from './App';
```

#### `importOrderTypeScriptVersion`

**type**: `string`
Expand Down
6 changes: 4 additions & 2 deletions docs/MIGRATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
- The `importOrderGroupNamespaceSpecifiers` option has been removed.
- The `importOrderSortSpecifiers` option has been removed, and specifiers are now always sorted (previous `true` setting)
- The `importOrderMergeDuplicateImports` option has been removed, and imports are always combined (previous `true` setting)
- The `importOrderCombineTypeAndValueImports` option has been removed. See [below](#importOrderCombineTypeAndValueImports-removed) for details
- The `importOrderCombineTypeAndValueImports` option has been removed. See [below](#importordercombinetypeandvalueimports-removed) for details
- Added `importOrderTypeScriptVersion` option.
- The default `importOrder` was improved. It now sorts node.js built-ins, then non-relative imports, then relative imports. If you have an `importOrder` specified, this will not affect you.

Expand All @@ -22,7 +22,9 @@ For example:

```js
"importOrder": [
"", // This emptry group at the start will add separators for side-effect imports and node.js built-in modules
"", // If you want a gap at the top after top-of-file-comments, put a separator here!
"<BUILTIN_MODULES>",
"",
"<THIRD_PARTY_MODULES>",
"",
"^@app/(.*)$",
Expand Down
9 changes: 8 additions & 1 deletion prettier.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ module.exports = {
bracketSameLine: true,
semi: true,
plugins: [require('./lib/src/index.js')],
importOrder: ['', '<THIRD_PARTY_MODULES>', '', '^[./]'],
importOrder: [
'',
'<BUILTIN_MODULES>',
'',
'<THIRD_PARTY_MODULES>',
'',
'^[./]',
],
importOrderTypeScriptVersion: '5.0.0',
};
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
36 changes: 35 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 All @@ -22,6 +23,15 @@ export interface PrettierOptions
extends Required<PluginConfig>,
RequiredOptions {}

/** Subset of options that need to be normalized, or affect normalization */
export type NormalizableOptions = Pick<
PrettierOptions,
| 'importOrder'
| 'importOrderParserPlugins'
| 'importOrderTypeScriptVersion'
| 'filepath'
>;

export type ChunkType = typeof chunkTypeOther | typeof chunkTypeUnsortable;
export type FlavorType =
| typeof importFlavorIgnore
Expand All @@ -46,13 +56,33 @@ export type SomeSpecifier =
| ImportNamespaceSpecifier;
export type ImportRelated = ImportOrLine | SomeSpecifier;

/**
* The PrettierOptions after validation/normalization
* - behavior flags are derived from the base options
* - plugins is dynamically modified by filepath
*/
export interface ExtendedOptions {
importOrder: PrettierOptions['importOrder'];
importOrderCombineTypeAndValueImports: boolean;
hasAnyCustomGroupSeparatorsInImportOrder: boolean;
provideGapAfterTopOfFileComments: boolean;
plugins: ParserPlugin[];
}

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

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

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

export type GetImportFlavorOfNode = (node: ImportDeclaration) => FlavorType;
Expand All @@ -65,3 +95,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

0 comments on commit bfda0f5

Please sign in to comment.