Skip to content

Commit a1d05d7

Browse files
Merge pull request #357 from ckwalsh/pr357
[Refactor] Stop rerendering directives, inject imports instead
2 parents 887b7eb + 4955de8 commit a1d05d7

File tree

9 files changed

+173
-159
lines changed

9 files changed

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

99
const generate = (generateModule as any).default || generateModule;
1010

@@ -15,31 +15,19 @@ const generate = (generateModule as any).default || generateModule;
1515
*/
1616
export const getCodeFromAst = (
1717
nodes: Statement[],
18-
directives: Directive[],
1918
originalCode: string,
20-
interpreter?: InterpreterDirective | null,
19+
injectIdx: number = 0,
2120
options?: Pick<PrettierOptions, 'importOrderImportAttributesKeyword'>,
2221
) => {
2322
const allCommentsFromImports = getAllCommentsFromNodes(nodes);
2423

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

3726
const newAST = file({
3827
type: 'Program',
3928
body: nodes,
40-
directives,
29+
directives: [],
4130
sourceType: 'module',
42-
interpreter: interpreter,
4331
leadingComments: [],
4432
innerComments: [],
4533
trailingComments: [],
@@ -57,10 +45,13 @@ export const getCodeFromAst = (
5745
importAttributesKeyword: options?.importOrderImportAttributesKeyword,
5846
});
5947

60-
return (
48+
return assembleUpdatedCode(
49+
originalCode,
50+
nodesToRemoveFromCode,
6151
code.replace(
6252
/"PRETTIER_PLUGIN_SORT_IMPORTS_NEW_LINE";/gi,
6353
newLineCharacters,
64-
) + codeWithoutImportsAndInterpreter.trim()
54+
),
55+
injectIdx,
6556
);
6657
};

0 commit comments

Comments
 (0)