Skip to content

Commit 2b4bf4a

Browse files
ckwalshvladislavarsenev
authored andcommitted
[Refactor] Stop rerendering directives, inject imports instead
Summary: This commit changes how the sorted imports are combined with the original source. Prior to this commit, all ImportDeclaration nodes and their leading comments, plus any InterpreterDirective and Directive nodes, were extracted from the original code and re-rendered using babel. The rendered nodes were then concatenated with the original source with those nodes removed to produce the updated source. This approach safely protected against functional changes, but removed newlines around comments near the beginning of the file when the first node of the original source was an ImportDeclaration, as babel does not preserve whitespace when rendering content. If a user has configured a plugin that attempts to manage comments and/or whitespace near the top of the file, such as auto-inserting a license header (as I am trying to do), this results in conflicts / formatting churn. This commit does not directly resolve this incompatibility, however it better prepares the codebase for a plugin option to be added that can resolve the issue. Test Plan: `yarn install && yarn run test --all` Note that one snapshot was changed by this commit where a newline was changed, acting as an effective example of how the original approach could affect whitespace in the re-rendered portion of the file.
1 parent 887b7eb commit 2b4bf4a

File tree

9 files changed

+170
-158
lines changed

9 files changed

+170
-158
lines changed

src/preprocessors/preprocessor.ts

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ParserOptions, parse as babelParser } from '@babel/parser';
2-
import { Directive, ImportDeclaration } from '@babel/types';
2+
import { ImportDeclaration } from '@babel/types';
33

44
import { PrettierOptions } from '../types';
55
import { extractASTNodes } from '../utils/extract-ast-nodes.js';
@@ -27,12 +27,11 @@ export function preprocessor(code: string, options: PrettierOptions) {
2727
};
2828

2929
const ast = babelParser(code, parserOptions);
30-
const interpreter = ast.program.interpreter;
3130

3231
const {
3332
importNodes,
34-
directives,
35-
}: { importNodes: ImportDeclaration[]; directives: Directive[] } =
33+
injectIdx,
34+
}: { importNodes: ImportDeclaration[]; injectIdx: number } =
3635
extractASTNodes(ast);
3736

3837
// short-circuit if there are no import declaration
@@ -49,7 +48,7 @@ export function preprocessor(code: string, options: PrettierOptions) {
4948
importOrderSideEffects,
5049
});
5150

52-
return getCodeFromAst(allImports, directives, code, interpreter, {
51+
return getCodeFromAst(allImports, code, injectIdx, {
5352
importOrderImportAttributesKeyword,
5453
});
5554
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
import { format } from 'prettier';
2+
3+
import { assembleUpdatedCode } from '../assemble-updated-code';
4+
import { getAllCommentsFromNodes } from '../get-all-comments-from-nodes';
5+
import { getImportNodes } from '../get-import-nodes';
6+
import { getSortedNodes } from '../get-sorted-nodes';
7+
8+
const code = `"use strict";
9+
// first comment
10+
// second comment
11+
import z from 'z';
12+
import c from 'c';
13+
import g from 'g';
14+
import t from 't';
15+
import k from 'k';
16+
// import a from 'a';
17+
// import a from 'a';
18+
import a from 'a';
19+
`;
20+
21+
test('it should remove nodes from the original code', async () => {
22+
const importNodes = getImportNodes(code);
23+
const sortedNodes = getSortedNodes(importNodes, {
24+
importOrder: [],
25+
importOrderCaseInsensitive: false,
26+
importOrderSeparation: false,
27+
importOrderGroupNamespaceSpecifiers: false,
28+
importOrderSortSpecifiers: false,
29+
importOrderSideEffects: true,
30+
});
31+
const allCommentsFromImports = getAllCommentsFromNodes(sortedNodes);
32+
33+
const commentAndImportsToRemoveFromCode = [
34+
...sortedNodes,
35+
...allCommentsFromImports,
36+
];
37+
const codeWithoutImportDeclarations = assembleUpdatedCode(
38+
code,
39+
commentAndImportsToRemoveFromCode,
40+
);
41+
const result = await format(codeWithoutImportDeclarations, {
42+
parser: 'babel',
43+
});
44+
expect(result).toEqual(`"use strict";
45+
`);
46+
});
47+
48+
test('it should inject the generated code at the correct location', async () => {
49+
const importNodes = getImportNodes(code);
50+
const sortedNodes = getSortedNodes(importNodes, {
51+
importOrder: [],
52+
importOrderCaseInsensitive: false,
53+
importOrderSeparation: false,
54+
importOrderGroupNamespaceSpecifiers: false,
55+
importOrderSortSpecifiers: false,
56+
importOrderSideEffects: true,
57+
});
58+
const allCommentsFromImports = getAllCommentsFromNodes(sortedNodes);
59+
60+
const commentAndImportsToRemoveFromCode = [
61+
...sortedNodes,
62+
...allCommentsFromImports,
63+
];
64+
const codeWithoutImportDeclarations = assembleUpdatedCode(
65+
code,
66+
commentAndImportsToRemoveFromCode,
67+
`import generated from "generated";`,
68+
'"use strict";'.length,
69+
);
70+
const result = await format(codeWithoutImportDeclarations, {
71+
parser: 'babel',
72+
});
73+
expect(result).toEqual(`"use strict";
74+
import generated from "generated";
75+
`);
76+
});
Lines changed: 1 addition & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
1-
import { ParserOptions, parse as babelParser } from '@babel/parser';
21
import { format } from 'prettier';
32
import { expect, test } from 'vitest';
43

5-
import { extractASTNodes } from '../extract-ast-nodes';
64
import { getCodeFromAst } from '../get-code-from-ast';
7-
import { getExperimentalParserPlugins } from '../get-experimental-parser-plugins';
85
import { getImportNodes } from '../get-import-nodes';
96
import { getSortedNodes } from '../get-sorted-nodes';
107

@@ -28,7 +25,7 @@ import a from 'a';
2825
importOrderSortByLength: null,
2926
importOrderSideEffects: true,
3027
});
31-
const formatted = getCodeFromAst(sortedNodes, [], code, null);
28+
const formatted = getCodeFromAst(sortedNodes, code);
3229
expect(await format(formatted, { parser: 'babel' })).toEqual(
3330
`// first comment
3431
// second comment
@@ -41,29 +38,3 @@ import z from "z";
4138
`,
4239
);
4340
});
44-
45-
test('it renders directives correctly', async () => {
46-
const code = `
47-
"use client";
48-
// first comment
49-
import b from 'b';
50-
import a from 'a';`;
51-
52-
const parserOptions: ParserOptions = {
53-
sourceType: 'module',
54-
plugins: getExperimentalParserPlugins([]),
55-
};
56-
const ast = babelParser(code, parserOptions);
57-
if (!ast) throw new Error('ast is null');
58-
const { directives, importNodes } = extractASTNodes(ast as any);
59-
60-
const formatted = getCodeFromAst(importNodes, directives, code, null);
61-
expect(await format(formatted, { parser: 'babel' })).toEqual(
62-
`"use client";
63-
64-
// first comment
65-
import b from "b";
66-
import a from "a";
67-
`,
68-
);
69-
});

src/utils/__tests__/remove-nodes-from-original-code.spec.ts

Lines changed: 0 additions & 46 deletions
This file was deleted.

src/utils/assemble-updated-code.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { Comment, Node } from '@babel/types';
2+
3+
type NodeOrComment = Node | Comment;
4+
type BoundedNodeOrComment = NodeOrComment & { start: number; end: number };
5+
6+
interface InjectedCode {
7+
type: 'InjectedCode';
8+
start: number;
9+
end: number;
10+
}
11+
12+
/**
13+
* Assembles the updated file, removing imports from the original file and
14+
* injecting the sorted imports at the appropriate location.
15+
*
16+
* @param code the whole file as text
17+
* @param nodes to be removed
18+
* @param injectedCode the generated import source to be injected
19+
* @param injectIdx the index at which to inject the generated source
20+
*/
21+
export const assembleUpdatedCode = (
22+
code: string,
23+
nodes: (Node | Comment)[],
24+
injectedCode?: string,
25+
injectIdx: number = 0,
26+
): string => {
27+
const ranges: (BoundedNodeOrComment | InjectedCode)[] = nodes.filter(
28+
(node): node is BoundedNodeOrComment => {
29+
const start = Number(node.start);
30+
const end = Number(node.end);
31+
return Number.isSafeInteger(start) && Number.isSafeInteger(end);
32+
},
33+
);
34+
if (injectedCode !== undefined) {
35+
ranges.push({
36+
type: 'InjectedCode',
37+
start: injectIdx,
38+
end: injectIdx,
39+
});
40+
}
41+
ranges.sort((a, b) => a.start - b.start);
42+
43+
let result: string = '';
44+
let idx = 0;
45+
46+
for (const { type, start, end } of ranges) {
47+
if (start > idx) {
48+
result += code.slice(idx, start);
49+
idx = start;
50+
}
51+
52+
if (injectedCode !== undefined && type === 'InjectedCode') {
53+
result += injectedCode;
54+
}
55+
56+
if (end > idx) {
57+
idx = end;
58+
}
59+
}
60+
61+
if (idx < code.length) {
62+
result += code.slice(idx);
63+
}
64+
65+
return result;
66+
};

src/utils/extract-ast-nodes.ts

Lines changed: 13 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,26 +1,23 @@
11
import { ParseResult } from '@babel/parser';
22
import traverseModule, { NodePath } from '@babel/traverse';
3-
import { Directive, File, ImportDeclaration } from '@babel/types';
3+
import { Directive, File, ImportDeclaration, Program } from '@babel/types';
44

55
const traverse = (traverseModule as any).default || traverseModule;
66

77
export function extractASTNodes(ast: ParseResult<File>) {
88
const importNodes: ImportDeclaration[] = [];
9-
const directives: Directive[] = [];
9+
let injectIdx = 0;
1010
traverse(ast, {
11-
Directive(path: NodePath<Directive>) {
12-
// Only capture directives if they are at the top scope of the source
13-
// and their previous siblings are all directives
14-
if (
15-
path.parent.type === 'Program' &&
16-
path.getAllPrevSiblings().every((s) => {
17-
return s.type === 'Directive';
18-
})
19-
) {
20-
directives.push(path.node);
21-
22-
// Trailing comments probably shouldn't be attached to the directive
23-
path.node.trailingComments = null;
11+
Program(path: NodePath<Program>) {
12+
/**
13+
* Imports will be injected before the first node of the body and
14+
* its comments, skipping InterpreterDirective and Directive nodes.
15+
* If the body is empty, default to 0, there will be no imports to
16+
* inject anyway.
17+
*/
18+
for (const node of path.node.body) {
19+
injectIdx = node.leadingComments?.[0]?.start ?? node.start ?? 0;
20+
break;
2421
}
2522
},
2623

@@ -33,5 +30,5 @@ export function extractASTNodes(ast: ParseResult<File>) {
3330
}
3431
},
3532
});
36-
return { importNodes, directives };
33+
return { importNodes, injectIdx };
3734
}

src/utils/get-code-from-ast.ts

Lines changed: 10 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import generateModule from '@babel/generator';
2-
import { Directive, InterpreterDirective, Statement, file } from '@babel/types';
2+
import { Statement, file } from '@babel/types';
33

44
import { newLineCharacters } from '../constants.js';
55
import { PrettierOptions } from '../types';
6+
import { assembleUpdatedCode } from './assemble-updated-code';
67
import { getAllCommentsFromNodes } from './get-all-comments-from-nodes.js';
78
import { removeNodesFromOriginalCode } from './remove-nodes-from-original-code.js';
89

@@ -15,31 +16,19 @@ const generate = (generateModule as any).default || generateModule;
1516
*/
1617
export const getCodeFromAst = (
1718
nodes: Statement[],
18-
directives: Directive[],
1919
originalCode: string,
20-
interpreter?: InterpreterDirective | null,
20+
injectIdx: number = 0,
2121
options?: Pick<PrettierOptions, 'importOrderImportAttributesKeyword'>,
2222
) => {
2323
const allCommentsFromImports = getAllCommentsFromNodes(nodes);
2424

25-
const nodesToRemoveFromCode = [
26-
...directives,
27-
...nodes,
28-
...allCommentsFromImports,
29-
...(interpreter ? [interpreter] : []),
30-
];
31-
32-
const codeWithoutImportsAndInterpreter = removeNodesFromOriginalCode(
33-
originalCode,
34-
nodesToRemoveFromCode,
35-
);
25+
const nodesToRemoveFromCode = [...nodes, ...allCommentsFromImports];
3626

3727
const newAST = file({
3828
type: 'Program',
3929
body: nodes,
40-
directives,
30+
directives: [],
4131
sourceType: 'module',
42-
interpreter: interpreter,
4332
leadingComments: [],
4433
innerComments: [],
4534
trailingComments: [],
@@ -57,10 +46,13 @@ export const getCodeFromAst = (
5746
importAttributesKeyword: options?.importOrderImportAttributesKeyword,
5847
});
5948

60-
return (
49+
return assembleUpdatedCode(
50+
originalCode,
51+
nodesToRemoveFromCode,
6152
code.replace(
6253
/"PRETTIER_PLUGIN_SORT_IMPORTS_NEW_LINE";/gi,
6354
newLineCharacters,
64-
) + codeWithoutImportsAndInterpreter.trim()
55+
),
56+
injectIdx,
6557
);
6658
};

0 commit comments

Comments
 (0)