-
Notifications
You must be signed in to change notification settings - Fork 59
/
ImportInjector.ts
110 lines (93 loc) · 3.2 KB
/
ImportInjector.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
import { NodePath } from '@babel/core';
import { isModule } from '@babel/helper-module-imports';
import * as t from '@babel/types';
// eslint-disable-next-line import/no-cycle
import { Change } from '../types';
import truthy from './truthy';
function findLast<T>(
array: T[],
predicate: (item: T, idx: number, list: T[]) => boolean,
): T | undefined {
for (let i = array.length - 1; i >= 0; --i) {
if (predicate(array[i], i, array)) return array[i];
}
return undefined;
}
function isRequire(path: NodePath<any>) {
const isRequireExpression = (p: any) =>
p.isCallExpression() &&
p.get('callee').isIdentifier() &&
(p.get('callee').node as any).name === 'require';
if (!path) return false;
if (path.isVariableDeclaration()) {
return (path as NodePath<t.VariableDeclaration>)
.get('declarations')
.some((d) => d && isRequireExpression(d.get('init')));
}
return isRequireExpression(path);
}
export default class StyleImportInjector {
private nodes = new Set<t.ImportDeclaration | t.VariableDeclaration>();
private code = new WeakMap<
t.ImportDeclaration | t.VariableDeclaration,
string
>();
constructor(private program: NodePath<t.Program>) {}
add(style: { identifier?: string; requirePath: string }) {
const { scope } = this.program;
const source = style.requirePath;
const useEsm = isModule(this.program);
const ident = scope.generateUidIdentifier(style.identifier);
if (useEsm) {
const importNode = t.importDeclaration(
[t.importDefaultSpecifier(ident)],
t.stringLiteral(source),
);
this.nodes.add(importNode);
this.code.set(importNode, `import ${ident.name} from "${source}";`);
} else {
const importNode = t.variableDeclaration('const', [
t.variableDeclarator(
ident,
t.callExpression(t.identifier('require'), [t.stringLiteral(source)]),
),
]);
this.nodes.add(importNode);
this.code.set(importNode, `const ${ident.name} = require("${source}");`);
}
return ident;
}
inject() {
const targetPath = findLast(this.program.get('body'), (p) => {
// this is a babel mechanism for sorting body blocks
// I don't want to rely on it, but do want to respect < 1 values as needing to go on the bottom
// we should not insert below those if possible
// https://github.com/babel/babel/blob/v7.8.5/packages/babel-core/src/transformation/block-hoist-plugin.js
//
// @ts-ignore
const blockHoist = p.node._blockHoist ?? 1; // eslint-disable-line no-underscore-dangle
return blockHoist > 0 && (p.isImportDeclaration() || isRequire(p));
});
const nodes = Array.from(this.nodes).reverse();
const end = targetPath?.node?.end || 0;
const changes: Change = {
type: 'style-imports',
end,
start: end,
code: `\n${Array.from(this.nodes, (n) => this.code.get(n))
.filter(truthy)
.join('\n')}\n`,
};
if (!targetPath) {
// @ts-ignore
this.program.unshiftContainer('body', nodes);
} else {
for (const node of nodes) {
targetPath!.insertAfter(node);
}
}
this.code = new WeakMap();
this.nodes.clear();
return changes;
}
}