forked from DomainGroupOSS/sass-to-emotion
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathjscodeshift.js
137 lines (112 loc) · 4.59 KB
/
jscodeshift.js
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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
/* eslint-disable no-param-reassign, no-console */
const glob = require('glob');
const path = require('path');
const selectorToLiteral = require('./selector-to-variable-identifier');
// TODO make CLI option
const STYLES_IMPORT_NAME = 'styles';
module.exports = (file, api) => {
const j = api.jscodeshift;
const root = j(file.source);
const hasReact = root
.find(j.ImportDeclaration, {
type: 'ImportDeclaration',
source: {
type: 'Literal',
value: 'react',
},
})
.size() === 1;
if (!hasReact) { console.info(`Skipping ${file.path} because it doesn't have React`); return null; }
const hasClassName = root
.find(j.JSXAttribute, {
name: {
type: 'JSXIdentifier',
name: 'className',
},
})
.size() > 0;
if (!hasClassName) { console.info(`Skipping ${file.path} because it has no JSX className attributes`); return null; }
const hasAStyleImport = root
.find(j.ImportDeclaration, {
type: 'ImportDeclaration',
source: {
type: 'Literal',
},
})
.filter(declarator => declarator.value.source.value.includes('styles'))
.size() > 0;
if (hasAStyleImport) { console.info(`Skipping ${file.path} because it already has a style import`); return null; }
const jsEmotionFiles = glob.sync(
path.join(file.path.split('/js/')[0], 'styles', '**', '*.js'),
{ absolute: true },
);
let moduleName;
// rename className="foo__bar-baz" => className={styles.barBaz}
root
.find(j.JSXAttribute, {
name: {
type: 'JSXIdentifier',
name: 'className',
},
})
.forEach((jsxPath) => {
if (jsxPath.value.value.type !== 'Literal') return;
const selector = jsxPath.value.value.value;
const memberExpressions = selector.split(' ').map((str) => {
const identifier = selectorToLiteral(str);
if (!moduleName) {
const pathToEmotionFile = jsEmotionFiles.find(
// eslint-disable-next-line import/no-dynamic-require, global-require
jsEmotionFile => !!require(jsEmotionFile)[identifier],
);
if (pathToEmotionFile) {
moduleName = path.join(
path.relative(path.dirname(file.path), path.dirname(pathToEmotionFile)),
path.basename(pathToEmotionFile),
).replace('.js', '');
}
}
return j.memberExpression(j.identifier(STYLES_IMPORT_NAME), j.identifier(identifier));
});
jsxPath.value.name = 'css';
jsxPath.value.value = j.jsxExpressionContainer(
memberExpressions.length > 1 ? j.arrayExpression(memberExpressions) : memberExpressions[0],
);
});
const collection = root.find(j.ImportDeclaration);
// I'd love to simply add to last import but insertAfter causing a line break after last import
// and before new styles import so code below gets weird
// https://github.com/facebook/jscodeshift/issues/185
const relativeImports = collection.filter(importPath => importPath.value.source.value.startsWith('.'));
let lastImportTarget;
if (relativeImports.length) {
lastImportTarget = relativeImports.get();
} else {
lastImportTarget = collection.at(-1).get();
}
const stylesImportStatement = j.importDeclaration(
[j.importNamespaceSpecifier(j.identifier(STYLES_IMPORT_NAME))],
j.literal(moduleName || '../../styles/FIXME'),
);
// e.g import foo from 'foo';
const lastImportIsSimpleModule = lastImportTarget.value.specifiers.length === 1
&& lastImportTarget.value.specifiers[0].type === 'ImportDefaultSpecifier';
// note linter wants relative import last
const lastImportIsRelative = lastImportTarget.value.source.value.startsWith('.');
if (lastImportIsRelative) {
j(lastImportTarget).insertBefore(stylesImportStatement);
} else if (lastImportIsSimpleModule) {
const oldImport = j.importDeclaration(
[j.importDefaultSpecifier(j.identifier(lastImportTarget.value.specifiers[0].local.name))],
j.literal(lastImportTarget.value.source.value),
);
j(lastImportTarget)
.replaceWith(stylesImportStatement)
// even if I use old lastImportTarget.value reference instead of oldImport, issue persists.
.insertBefore(oldImport);
} else {
// this will cause a line break after last import, seems FB strugled with this one too https://github.com/reactjs/react-codemod/blob/96b55a0ea70c7b1a9c64d12b47e523804bb74b22/transforms/__testfixtures__/ReactNative-View-propTypes/default-import-multi-reference.output.js#L4
j(lastImportTarget).insertAfter(stylesImportStatement);
}
return root.toSource({ quote: 'single', trailingComma: true });
};