app-studio 0.1.18 → 0.1.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/codemod/CHANGELOG.md +32 -0
- package/codemod/README.md +52 -0
- package/codemod/bin/app-studio-codemod.js +12 -0
- package/codemod/bin/cli.js +200 -0
- package/codemod/package.json +41 -0
- package/codemod/transforms/__tests__/to-app-studio.test.js +268 -0
- package/codemod/transforms/to-app-studio.ts +486 -0
- package/codemod/transforms/utils.ts +267 -0
- package/codemod/tsconfig.json +12 -0
- package/package.json +3 -2
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# Change Log
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
|
+
|
|
6
|
+
## [0.1.1](https://github.com/reflexjs/reflexjs/compare/@reflexjs/codemod@0.1.0...@reflexjs/codemod@0.1.1) (2021-03-11)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
11
|
+
* update repository name for all packages ([fdc25b0](https://github.com/reflexjs/reflexjs/commit/fdc25b02d1008749a36e2c9027a701fc6a2c0168))
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# 0.1.0 (2020-12-03)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
### Bug Fixes
|
|
21
|
+
|
|
22
|
+
* **www:** update to reflexjs ([251deb5](https://github.com/reflexjs/reflex/commit/251deb5fd6df6c7155eedb401ea1eccc0f9a5ef2))
|
|
23
|
+
* update example-video ([449fe8d](https://github.com/reflexjs/reflex/commit/449fe8da1d9b188d66ca1a07d2ec8a457593f2fc))
|
|
24
|
+
* **codemod:** check for name ([6166b20](https://github.com/reflexjs/reflex/commit/6166b20eee9b0b18f14fb9d05a87d71b39ffca93))
|
|
25
|
+
* **codemod:** update bin ([2e569b3](https://github.com/reflexjs/reflex/commit/2e569b3a41991070b091e19f83dd273671dc4663))
|
|
26
|
+
* **codemod:** update tests ([2b9086e](https://github.com/reflexjs/reflex/commit/2b9086e7cd6783898e0fd5c3eef84fbeb72d906e))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
### Features
|
|
30
|
+
|
|
31
|
+
* **codemod:** change Container to variant container ([33563d0](https://github.com/reflexjs/reflex/commit/33563d06087a2c7762a6b26027ef9677acc579c3))
|
|
32
|
+
* add codemods ([9292862](https://github.com/reflexjs/reflex/commit/9292862ceb82d81efc1bd64cbcace240d7a40550))
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# @app-studio/codemod
|
|
2
|
+
|
|
3
|
+
Provides Codemod transformations to help with code upgrade and migration.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npx @app-studio/codemod <transform> <path
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
- `transform` - name of the transform
|
|
12
|
+
- `path` - files or directory to transform
|
|
13
|
+
- `--dry` - Performs a dry-run, no code changes
|
|
14
|
+
- `--print` - Prints the changes for comparison
|
|
15
|
+
|
|
16
|
+
## Codemods
|
|
17
|
+
|
|
18
|
+
### `to-reflexjs`
|
|
19
|
+
|
|
20
|
+
This Codemod migrates your `@app-studio/components` code to `app-studio` code.
|
|
21
|
+
|
|
22
|
+
```js
|
|
23
|
+
npx @reflexjs/codemod to-app-studio
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Example:
|
|
27
|
+
|
|
28
|
+
```jsx
|
|
29
|
+
import { Div, H1, Button } from "@app-studio/components"
|
|
30
|
+
|
|
31
|
+
export default function () {
|
|
32
|
+
return (
|
|
33
|
+
<Div d="flex">
|
|
34
|
+
<H1>This is a heading</H1>
|
|
35
|
+
<Button variant="primary lg">Button</Button>
|
|
36
|
+
</Div>
|
|
37
|
+
)
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Will be transformed to:
|
|
42
|
+
|
|
43
|
+
```jsx
|
|
44
|
+
export default function () {
|
|
45
|
+
return (
|
|
46
|
+
<div display="flex">
|
|
47
|
+
<h1 variant="heading.h1">This is a heading</h1>
|
|
48
|
+
<button variant="button.primary.lg">Button</button>
|
|
49
|
+
</div>
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
```
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Copyright 2015-present, Facebook, Inc.
|
|
5
|
+
*
|
|
6
|
+
* This source code is licensed under the MIT license found in the
|
|
7
|
+
* LICENSE file in the root directory of this source tree.
|
|
8
|
+
*
|
|
9
|
+
*/
|
|
10
|
+
// Based on https://github.com/reactjs/react-codemod/blob/dd8671c9a470a2c342b221ec903c574cf31e9f57/bin/react-codemod.js
|
|
11
|
+
|
|
12
|
+
require('./cli').run();
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright 2015-present, Facebook, Inc.
|
|
3
|
+
*
|
|
4
|
+
* This source code is licensed under the MIT license found in the
|
|
5
|
+
* LICENSE file in the root directory of this source tree.
|
|
6
|
+
*
|
|
7
|
+
*/
|
|
8
|
+
// Based on https://github.com/reactjs/react-codemod/blob/dd8671c9a470a2c342b221ec903c574cf31e9f57/bin/cli.js
|
|
9
|
+
// @next/codemod optional-name-of-transform optional/path/to/src [...options]
|
|
10
|
+
|
|
11
|
+
const globby = require('globby');
|
|
12
|
+
const inquirer = require('inquirer');
|
|
13
|
+
const meow = require('meow');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const execa = require('execa');
|
|
16
|
+
const chalk = require('chalk');
|
|
17
|
+
const isGitClean = require('is-git-clean');
|
|
18
|
+
|
|
19
|
+
const transformerDirectory = path.join(__dirname, '../', 'transforms');
|
|
20
|
+
const jscodeshiftExecutable = require.resolve('.bin/jscodeshift');
|
|
21
|
+
|
|
22
|
+
function checkGitStatus(force) {
|
|
23
|
+
let clean = false;
|
|
24
|
+
let errorMessage = 'Unable to determine if git directory is clean';
|
|
25
|
+
try {
|
|
26
|
+
clean = isGitClean.sync(process.cwd());
|
|
27
|
+
errorMessage = 'Git directory is not clean';
|
|
28
|
+
} catch (err) {
|
|
29
|
+
if (err && err.stderr && err.stderr.indexOf('Not a git repository') >= 0) {
|
|
30
|
+
clean = true;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!clean) {
|
|
35
|
+
if (force) {
|
|
36
|
+
console.log(`WARNING: ${errorMessage}. Forcibly continuing.`);
|
|
37
|
+
} else {
|
|
38
|
+
console.log('Thank you for using app-studio/codemod!');
|
|
39
|
+
console.log(
|
|
40
|
+
chalk.yellow(
|
|
41
|
+
'\nBut before we continue, please stash or commit your git changes.'
|
|
42
|
+
)
|
|
43
|
+
);
|
|
44
|
+
console.log(
|
|
45
|
+
'\nYou may use the --force flag to override this safety check.'
|
|
46
|
+
);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function runTransform({ files, flags, transformer }) {
|
|
53
|
+
const transformerPath = path.join(transformerDirectory, `${transformer}.js`);
|
|
54
|
+
|
|
55
|
+
let args = [];
|
|
56
|
+
|
|
57
|
+
const { dry, print } = flags;
|
|
58
|
+
|
|
59
|
+
if (dry) {
|
|
60
|
+
args.push('--dry');
|
|
61
|
+
}
|
|
62
|
+
if (print) {
|
|
63
|
+
args.push('--print');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
args.push('--verbose=0');
|
|
67
|
+
|
|
68
|
+
args.push('--ignore-pattern=**/node_modules/**');
|
|
69
|
+
args.push('--ignore-pattern=**/.next/**');
|
|
70
|
+
args.push('--ignore-pattern=**/.cache/**');
|
|
71
|
+
|
|
72
|
+
args.push('--extensions=tsx,ts,jsx,js,mdx');
|
|
73
|
+
args.push('--parser=tsx');
|
|
74
|
+
|
|
75
|
+
args = args.concat(['--transform', transformerPath]);
|
|
76
|
+
|
|
77
|
+
if (flags.jscodeshift) {
|
|
78
|
+
args = args.concat(flags.jscodeshift);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
args = args.concat(files);
|
|
82
|
+
|
|
83
|
+
console.log(`Executing command: jscodeshift ${args.join(' ')}`);
|
|
84
|
+
|
|
85
|
+
const result = execa.sync(jscodeshiftExecutable, args, {
|
|
86
|
+
stdio: 'inherit',
|
|
87
|
+
stripEof: false,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (result.error) {
|
|
91
|
+
throw result.error;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const TRANSFORMER_INQUIRER_CHOICES = [
|
|
96
|
+
{
|
|
97
|
+
name: 'to-app-studio: Transforms styled-components to App Studio View components.',
|
|
98
|
+
value: 'to-app-studio', // This should correspond to a file named "to-app-studio.js" in your "transforms" directory
|
|
99
|
+
},
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
function expandFilePathsIfNeeded(filesBeforeExpansion) {
|
|
103
|
+
const shouldExpandFiles = filesBeforeExpansion.some((file) =>
|
|
104
|
+
file.includes('*')
|
|
105
|
+
);
|
|
106
|
+
return shouldExpandFiles
|
|
107
|
+
? globby.sync(filesBeforeExpansion)
|
|
108
|
+
: filesBeforeExpansion;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function run() {
|
|
112
|
+
const cli = meow(
|
|
113
|
+
{
|
|
114
|
+
description: 'Codemods for updating Reflexjs apps.',
|
|
115
|
+
help: `
|
|
116
|
+
Usage
|
|
117
|
+
$ npx app-studio/codemod <transform> <path> <...options>
|
|
118
|
+
transform Name of the transform
|
|
119
|
+
path Files or directory to transform. Can be a glob like pages/**.js
|
|
120
|
+
Options
|
|
121
|
+
--force Bypass Git safety checks and forcibly run codemods
|
|
122
|
+
--dry Dry run (no changes are made to files)
|
|
123
|
+
--print Print transformed files to your terminal
|
|
124
|
+
--jscodeshift (Advanced) Pass options directly to jscodeshift
|
|
125
|
+
`,
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
boolean: ['force', 'dry', 'print', 'help'],
|
|
129
|
+
string: ['_'],
|
|
130
|
+
alias: {
|
|
131
|
+
h: 'help',
|
|
132
|
+
},
|
|
133
|
+
}
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
if (!cli.flags.dry) {
|
|
137
|
+
checkGitStatus(cli.flags.force);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (
|
|
141
|
+
cli.input[0] &&
|
|
142
|
+
!TRANSFORMER_INQUIRER_CHOICES.find((x) => x.value === cli.input[0])
|
|
143
|
+
) {
|
|
144
|
+
console.error('Invalid transform choice, pick one of:');
|
|
145
|
+
console.error(
|
|
146
|
+
TRANSFORMER_INQUIRER_CHOICES.map((x) => '- ' + x.value).join('\n')
|
|
147
|
+
);
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
inquirer
|
|
152
|
+
.prompt([
|
|
153
|
+
{
|
|
154
|
+
type: 'input',
|
|
155
|
+
name: 'files',
|
|
156
|
+
message: 'On which files or directory should the codemods be applied?',
|
|
157
|
+
when: !cli.input[1],
|
|
158
|
+
default: '.',
|
|
159
|
+
// validate: () =>
|
|
160
|
+
filter: (files) => files.trim(),
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
type: 'list',
|
|
164
|
+
name: 'transformer',
|
|
165
|
+
message: 'Which transform would you like to apply?',
|
|
166
|
+
when: !cli.input[0],
|
|
167
|
+
pageSize: TRANSFORMER_INQUIRER_CHOICES.length,
|
|
168
|
+
choices: TRANSFORMER_INQUIRER_CHOICES,
|
|
169
|
+
},
|
|
170
|
+
])
|
|
171
|
+
.then((answers) => {
|
|
172
|
+
const { files, transformer } = answers;
|
|
173
|
+
|
|
174
|
+
const filesBeforeExpansion = cli.input[1] || files;
|
|
175
|
+
const filesExpanded = expandFilePathsIfNeeded([filesBeforeExpansion]);
|
|
176
|
+
|
|
177
|
+
const selectedTransformer = cli.input[0] || transformer;
|
|
178
|
+
|
|
179
|
+
if (!filesExpanded.length) {
|
|
180
|
+
console.log(
|
|
181
|
+
`No files found matching ${filesBeforeExpansion.join(' ')}`
|
|
182
|
+
);
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return runTransform({
|
|
187
|
+
files: filesExpanded,
|
|
188
|
+
flags: cli.flags,
|
|
189
|
+
transformer: selectedTransformer,
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
module.exports = {
|
|
195
|
+
run: run,
|
|
196
|
+
runTransform: runTransform,
|
|
197
|
+
checkGitStatus: checkGitStatus,
|
|
198
|
+
jscodeshiftExecutable: jscodeshiftExecutable,
|
|
199
|
+
transformerDirectory: transformerDirectory,
|
|
200
|
+
};
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@app-studio/codemod",
|
|
3
|
+
"description": "Migration helpers for app-studio",
|
|
4
|
+
"version": "0.1.1",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"publishConfig": {
|
|
7
|
+
"access": "public"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"watch": "yarn tsc -d -w -p tsconfig.json",
|
|
11
|
+
"prepare": "yarn tsc -d -p tsconfig.json",
|
|
12
|
+
"test": "jest"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"transforms/*.js",
|
|
16
|
+
"bin/*.js"
|
|
17
|
+
],
|
|
18
|
+
"bin": "./bin/app-studio-codemod.js",
|
|
19
|
+
"bugs": {
|
|
20
|
+
"url": "https://github.com/rize-network/app-studio/issues"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/rize-network/app-studio",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/rize-network/app-studio.git",
|
|
26
|
+
"directory": "packages/codemod"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"chalk": "^4.1.0",
|
|
30
|
+
"execa": "^4.1.0",
|
|
31
|
+
"globby": "^11.0.1",
|
|
32
|
+
"gray-matter": "^4.0.2",
|
|
33
|
+
"inquirer": "^7.3.3",
|
|
34
|
+
"is-git-clean": "^1.1.0",
|
|
35
|
+
"jscodeshift": "^0.11.0",
|
|
36
|
+
"meow": "^8.0.0"
|
|
37
|
+
},
|
|
38
|
+
"devDependencies": {
|
|
39
|
+
"@types/jscodeshift": "^0.7.1"
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
const j = require('jscodeshift');
|
|
2
|
+
const {
|
|
3
|
+
transformStyledToView,
|
|
4
|
+
transformHTMLToView,
|
|
5
|
+
transformStyleToProps,
|
|
6
|
+
COMPONENT_MAPPING,
|
|
7
|
+
} = require('./to-app-studio.test'); // Assurez-vous d'exporter la fonction
|
|
8
|
+
|
|
9
|
+
describe('transformHTMLToComponent', () => {
|
|
10
|
+
it('should transform html tag to component', () => {
|
|
11
|
+
const sourceCode = `<button className="some-class"></button>`;
|
|
12
|
+
const root = j(sourceCode);
|
|
13
|
+
transformHTMLToView(root, j);
|
|
14
|
+
|
|
15
|
+
expect(root.toSource()).toEqual(`
|
|
16
|
+
import {${COMPONENT_MAPPING['button']}} from '@app-studio/components'
|
|
17
|
+
|
|
18
|
+
<${COMPONENT_MAPPING['button']} className="some-class"></${COMPONENT_MAPPING['button']}>`);
|
|
19
|
+
});
|
|
20
|
+
// Ajoutez d'autres cas de test pour d'autres transformations si nécessaire
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
describe('transformHTMLToView', () => {
|
|
24
|
+
it('should transform html tag to View with as attribute', () => {
|
|
25
|
+
const sourceCode = `<button className="some-class"></button>`;
|
|
26
|
+
const root = j(sourceCode);
|
|
27
|
+
transformHTMLToView(root, j);
|
|
28
|
+
|
|
29
|
+
expect(root.toSource()).toEqual(
|
|
30
|
+
'<View as="button" className="some-class"></View>'
|
|
31
|
+
);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should transform div to Div', () => {
|
|
35
|
+
const sourceCode = `<div className="some-class"></div>`;
|
|
36
|
+
const root = j(sourceCode);
|
|
37
|
+
transformHTMLToView(root, j);
|
|
38
|
+
|
|
39
|
+
expect(root.toSource()).toEqual('<Div className="some-class"></View>');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('should transform div to Div', () => {
|
|
43
|
+
const sourceCode = `<div className="some-class"></div>`;
|
|
44
|
+
const root = j(sourceCode);
|
|
45
|
+
transformHTMLToView(root, j);
|
|
46
|
+
|
|
47
|
+
expect(root.toSource()).toEqual('<Div className="some-class"></View>');
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should transform span to View with as attribute', () => {
|
|
51
|
+
const sourceCode = `<span></span>`;
|
|
52
|
+
const root = j(sourceCode);
|
|
53
|
+
transformHTMLToView(root, j);
|
|
54
|
+
|
|
55
|
+
expect(root.toSource()).toEqual('<View as="span"></View>');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Ajoutez d'autres cas de test pour d'autres transformations si nécessaire
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('transformStyledToView', () => {
|
|
62
|
+
it('should transform css to props', () => {
|
|
63
|
+
const input = `
|
|
64
|
+
import styled from 'styled-component'
|
|
65
|
+
|
|
66
|
+
const Container = styled.div\`
|
|
67
|
+
background-color: blue;
|
|
68
|
+
color: white:
|
|
69
|
+
\`;
|
|
70
|
+
|
|
71
|
+
const App = () => <Container />;
|
|
72
|
+
`;
|
|
73
|
+
|
|
74
|
+
const root = j(input);
|
|
75
|
+
transformStyledToView(root, j);
|
|
76
|
+
|
|
77
|
+
expect(root.toSource()).toEqual(`
|
|
78
|
+
import {View} from 'app-studio'
|
|
79
|
+
|
|
80
|
+
const Container = (props) => <View
|
|
81
|
+
backgroundColor='blue'
|
|
82
|
+
color="white" {...props} />;
|
|
83
|
+
|
|
84
|
+
const App = () => <Container />;
|
|
85
|
+
`);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('should handle multiple events', () => {
|
|
89
|
+
const input = `
|
|
90
|
+
import styled from 'styled-component'
|
|
91
|
+
|
|
92
|
+
const Container = styled.div\`
|
|
93
|
+
:hover {
|
|
94
|
+
color: red;
|
|
95
|
+
}
|
|
96
|
+
:active {
|
|
97
|
+
color: blue;
|
|
98
|
+
}
|
|
99
|
+
\`;
|
|
100
|
+
|
|
101
|
+
const App = () => <Container />;
|
|
102
|
+
`;
|
|
103
|
+
|
|
104
|
+
const root = j(input);
|
|
105
|
+
transformStyledToView(root, j);
|
|
106
|
+
|
|
107
|
+
expect(root.toSource()).toEqual(`
|
|
108
|
+
import {View} from 'app-studio'
|
|
109
|
+
|
|
110
|
+
const Container = (props) => <View
|
|
111
|
+
on={{
|
|
112
|
+
hover: {color: 'red'},
|
|
113
|
+
active: {color: 'blue'}
|
|
114
|
+
}}
|
|
115
|
+
{...props} />;
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
const App = () => <Container />;
|
|
119
|
+
`);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should transform media query to props with breakpoints', () => {
|
|
123
|
+
const breakpoints = {
|
|
124
|
+
xs: 0,
|
|
125
|
+
sm: 340,
|
|
126
|
+
md: 560,
|
|
127
|
+
lg: 1080,
|
|
128
|
+
xl: 1300,
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const input = `
|
|
132
|
+
import styled from 'styled-component'
|
|
133
|
+
|
|
134
|
+
const Container = styled.div\`
|
|
135
|
+
background-color: blue;
|
|
136
|
+
@media (min-width: ${breakpoints.md.toString()} px) {
|
|
137
|
+
background-color: red;
|
|
138
|
+
}
|
|
139
|
+
\`;
|
|
140
|
+
|
|
141
|
+
const App = () => <Container />;
|
|
142
|
+
`;
|
|
143
|
+
|
|
144
|
+
const root = j(input);
|
|
145
|
+
transformStyledToView(root, j);
|
|
146
|
+
|
|
147
|
+
expect(root.toSource()).toEqual(`
|
|
148
|
+
import {View} from 'app-studio'
|
|
149
|
+
|
|
150
|
+
const Container = (props) => <View
|
|
151
|
+
backgroundColor='blue'
|
|
152
|
+
media={{
|
|
153
|
+
md : 'red'
|
|
154
|
+
}}
|
|
155
|
+
{...props} />;
|
|
156
|
+
|
|
157
|
+
const App = () => <Container />;
|
|
158
|
+
`);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should transform media query to props with breakpoints', () => {
|
|
162
|
+
const breakpoints = {
|
|
163
|
+
xs: 0,
|
|
164
|
+
sm: 340,
|
|
165
|
+
md: 560,
|
|
166
|
+
lg: 1080,
|
|
167
|
+
xl: 1300,
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
const devices = {
|
|
171
|
+
mobile: ['xs', 'sm'],
|
|
172
|
+
tablet: ['md', 'lg'],
|
|
173
|
+
desktop: ['lg', 'xl'],
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
const input = `
|
|
177
|
+
import styled from 'styled-component'
|
|
178
|
+
|
|
179
|
+
const Container = styled.div\`
|
|
180
|
+
background-color: blue;
|
|
181
|
+
@media (min-width: ${Math.min(
|
|
182
|
+
devices.mobile.map((v) => breakpoints[v])
|
|
183
|
+
).toString()}px and ${Math.max(
|
|
184
|
+
devices.mobile.map((v) => breakpoints[v])
|
|
185
|
+
).toString()}px) {
|
|
186
|
+
background-color: red;
|
|
187
|
+
}
|
|
188
|
+
\`;
|
|
189
|
+
|
|
190
|
+
const App = () => <Container />;
|
|
191
|
+
`;
|
|
192
|
+
|
|
193
|
+
const root = j(input);
|
|
194
|
+
transformStyledToView(root, j);
|
|
195
|
+
|
|
196
|
+
expect(root.toSource()).toEqual(`
|
|
197
|
+
import {View} from 'app-studio'
|
|
198
|
+
|
|
199
|
+
const Container = (props) => <View
|
|
200
|
+
backgroundColor='blue'
|
|
201
|
+
media={{
|
|
202
|
+
mobile : {
|
|
203
|
+
backgroundColor:'red'
|
|
204
|
+
}
|
|
205
|
+
}}
|
|
206
|
+
{...props} />;
|
|
207
|
+
|
|
208
|
+
const App = () => <Container />;
|
|
209
|
+
`);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe('transformStyleToProps', () => {
|
|
214
|
+
it('should transform style (text) to props', () => {
|
|
215
|
+
const input = `
|
|
216
|
+
const Container = (props) => <div
|
|
217
|
+
style="background-color: blue;color: white;"
|
|
218
|
+
{...props}
|
|
219
|
+
/>;
|
|
220
|
+
|
|
221
|
+
const App = () => <Container />;
|
|
222
|
+
`;
|
|
223
|
+
|
|
224
|
+
const root = j(input);
|
|
225
|
+
transformStyleToProps(root, j);
|
|
226
|
+
|
|
227
|
+
expect(root.toSource()).toEqual(`
|
|
228
|
+
import {View} from 'app-studio'
|
|
229
|
+
|
|
230
|
+
const Container = (props) => <View
|
|
231
|
+
backgroundColor='blue'
|
|
232
|
+
color='white'
|
|
233
|
+
{...props}
|
|
234
|
+
/>;
|
|
235
|
+
|
|
236
|
+
const App = () => <Container />;
|
|
237
|
+
`);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('should transform style (object) to props', () => {
|
|
241
|
+
const input = `
|
|
242
|
+
const Container = (props) => <div
|
|
243
|
+
style={{
|
|
244
|
+
backgroundColor: 'blue',
|
|
245
|
+
color: 'white'
|
|
246
|
+
}}
|
|
247
|
+
{...props}
|
|
248
|
+
/>;
|
|
249
|
+
|
|
250
|
+
const App = () => <Container />;
|
|
251
|
+
`;
|
|
252
|
+
|
|
253
|
+
const root = j(input);
|
|
254
|
+
transformStyleToProps(root, j);
|
|
255
|
+
|
|
256
|
+
expect(root.toSource()).toEqual(`
|
|
257
|
+
import {View} from 'app-studio'
|
|
258
|
+
|
|
259
|
+
const Container = (props) => <View
|
|
260
|
+
backgroundColor='blue'
|
|
261
|
+
color="white"
|
|
262
|
+
{...props}
|
|
263
|
+
/>;
|
|
264
|
+
|
|
265
|
+
const App = () => <Container />;
|
|
266
|
+
`);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import postcss from 'postcss';
|
|
4
|
+
|
|
5
|
+
export const APP_MAPPING = {
|
|
6
|
+
a: 'A',
|
|
7
|
+
img: 'Image',
|
|
8
|
+
div: 'View',
|
|
9
|
+
span: 'Span',
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export const COMPONENT_MAPPING = {
|
|
13
|
+
textarea: 'Textarea',
|
|
14
|
+
svg: 'Svg',
|
|
15
|
+
select: 'Select',
|
|
16
|
+
picture: 'Image',
|
|
17
|
+
option: 'Option',
|
|
18
|
+
map: 'Map',
|
|
19
|
+
input: 'Input',
|
|
20
|
+
iframe: 'Iframe',
|
|
21
|
+
form: 'Form',
|
|
22
|
+
button: 'Button',
|
|
23
|
+
audio: 'Audio',
|
|
24
|
+
video: 'Video',
|
|
25
|
+
img: 'Image',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// eslint-disable-next-line prefer-const
|
|
29
|
+
let IMPORT_APP = {};
|
|
30
|
+
// eslint-disable-next-line prefer-const
|
|
31
|
+
let IMPORT_COMPONENT = {};
|
|
32
|
+
|
|
33
|
+
function isHtmlElement(elementName) {
|
|
34
|
+
return elementName[0] === elementName[0].toLowerCase();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function transformStyledComponentsToView(root, j, imports) {
|
|
38
|
+
root
|
|
39
|
+
.find(j.TaggedTemplateExpression)
|
|
40
|
+
.filter((path) => {
|
|
41
|
+
return (
|
|
42
|
+
path.node.tag.type === 'Identifier' && path.node.tag.name === 'styled'
|
|
43
|
+
);
|
|
44
|
+
})
|
|
45
|
+
.forEach((path) => {
|
|
46
|
+
const quasis = path.node.quasi.quasis;
|
|
47
|
+
|
|
48
|
+
// eslint-disable-next-line prefer-const
|
|
49
|
+
let mediaQueries = {};
|
|
50
|
+
let rootStyles = {};
|
|
51
|
+
|
|
52
|
+
quasis.forEach((quasi) => {
|
|
53
|
+
const cssString = quasi.value.raw;
|
|
54
|
+
|
|
55
|
+
// Extrait les media queries
|
|
56
|
+
const mediaRegex =
|
|
57
|
+
/@media \(?(min-width:\s*(\d+)px)?\s*(and)?\s*(max-width:\s*(\d+)px)?\)?\s*\{([\s\S]*?)\}/g;
|
|
58
|
+
let match;
|
|
59
|
+
|
|
60
|
+
while ((match = mediaRegex.exec(cssString)) !== null) {
|
|
61
|
+
const minWidth = match[2] ? parseInt(match[2], 10) : null;
|
|
62
|
+
const maxWidth = match[5] ? parseInt(match[5], 10) : null;
|
|
63
|
+
const stylesString = match[6].trim();
|
|
64
|
+
|
|
65
|
+
let breakpointName = 'xs';
|
|
66
|
+
|
|
67
|
+
if (minWidth) {
|
|
68
|
+
breakpointName += `min[${minWidth}]`;
|
|
69
|
+
}
|
|
70
|
+
if (maxWidth) {
|
|
71
|
+
breakpointName += `max[${maxWidth}]`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const styles = stylesString
|
|
75
|
+
.split(';')
|
|
76
|
+
.filter(Boolean)
|
|
77
|
+
.reduce((acc, style) => {
|
|
78
|
+
const [key, value] = style.split(':').map((str) => str.trim());
|
|
79
|
+
if (key && value) {
|
|
80
|
+
acc[key] = value;
|
|
81
|
+
}
|
|
82
|
+
return acc;
|
|
83
|
+
}, {});
|
|
84
|
+
|
|
85
|
+
mediaQueries[breakpointName] = styles;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Extrait les styles root
|
|
89
|
+
const rootRegex = /([^@{}]+)\{([\s\S]*?)\}/g;
|
|
90
|
+
while ((match = rootRegex.exec(cssString)) !== null) {
|
|
91
|
+
const stylesString = match[2].trim();
|
|
92
|
+
const styles = stylesString
|
|
93
|
+
.split(';')
|
|
94
|
+
.filter(Boolean)
|
|
95
|
+
.reduce((acc, style) => {
|
|
96
|
+
const [key, value] = style.split(':').map((str) => str.trim());
|
|
97
|
+
if (key && value) {
|
|
98
|
+
acc[key] = value;
|
|
99
|
+
}
|
|
100
|
+
return acc;
|
|
101
|
+
}, {});
|
|
102
|
+
rootStyles = { ...rootStyles, ...styles };
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
let attributes = [j.jsxSpreadAttribute(j.identifier('props'))];
|
|
107
|
+
|
|
108
|
+
if (Object.keys(mediaQueries).length > 0) {
|
|
109
|
+
const mediaProp = j.jsxAttribute(
|
|
110
|
+
j.jsxIdentifier('media')
|
|
111
|
+
// ... (le même code pour créer la prop 'media') ...
|
|
112
|
+
);
|
|
113
|
+
attributes.push(mediaProp);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (Object.keys(rootStyles).length > 0) {
|
|
117
|
+
const rootStyleProps = Object.keys(rootStyles).map((key) =>
|
|
118
|
+
j.jsxAttribute(j.jsxIdentifier(key), j.literal(rootStyles[key]))
|
|
119
|
+
);
|
|
120
|
+
attributes = [...attributes, ...rootStyleProps];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
path.node.init = j.arrowFunctionExpression(
|
|
124
|
+
[j.identifier('props')],
|
|
125
|
+
j.jsxElement(
|
|
126
|
+
j.jsxOpeningElement(j.jsxIdentifier('View'), attributes),
|
|
127
|
+
j.jsxClosingElement(j.jsxIdentifier('View')),
|
|
128
|
+
[]
|
|
129
|
+
)
|
|
130
|
+
);
|
|
131
|
+
imports['View'] = true;
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function transformStyleToProps(root, j, imports) {
|
|
136
|
+
root.find(j.JSXAttribute, { name: { name: 'style' } }).forEach((path) => {
|
|
137
|
+
const attrValue = path.node.value;
|
|
138
|
+
// eslint-disable-next-line prefer-const
|
|
139
|
+
let newAttributes = [];
|
|
140
|
+
|
|
141
|
+
if (attrValue.type === 'Literal') {
|
|
142
|
+
const inlineStyles = attrValue.value.split(';').filter(Boolean);
|
|
143
|
+
inlineStyles.forEach((inlineStyle) => {
|
|
144
|
+
const [key, value] = inlineStyle.split(':').map((s) => s.trim());
|
|
145
|
+
const propName = key.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
|
|
146
|
+
newAttributes.push(
|
|
147
|
+
j.jsxAttribute(j.jsxIdentifier(propName), j.stringLiteral(value))
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
} else if (
|
|
151
|
+
attrValue.type === 'JSXExpressionContainer' &&
|
|
152
|
+
attrValue.expression.type === 'ObjectExpression'
|
|
153
|
+
) {
|
|
154
|
+
attrValue.expression.properties.forEach((prop) => {
|
|
155
|
+
const propName = prop.key.name;
|
|
156
|
+
const propValue = prop.value;
|
|
157
|
+
newAttributes.push(
|
|
158
|
+
j.jsxAttribute(j.jsxIdentifier(propName), propValue)
|
|
159
|
+
);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const openingElement = path.parentPath.node;
|
|
164
|
+
openingElement.attributes = openingElement.attributes.filter(
|
|
165
|
+
(attr) => attr.name.name !== 'style'
|
|
166
|
+
);
|
|
167
|
+
openingElement.attributes.push(...newAttributes);
|
|
168
|
+
imports['View'] = true;
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Fonction pour ajouter une déclaration d'importation si elle n'est pas déjà présente
|
|
173
|
+
export function addImportStatement(root, j, importNames = [], fromModule) {
|
|
174
|
+
const existingImport = root.find(j.ImportDeclaration, {
|
|
175
|
+
source: { value: fromModule },
|
|
176
|
+
});
|
|
177
|
+
if (existingImport.size() === 0) {
|
|
178
|
+
const importStatement = j.importDeclaration(
|
|
179
|
+
importNames.map((importName) =>
|
|
180
|
+
j.importSpecifier(j.identifier(importName))
|
|
181
|
+
),
|
|
182
|
+
j.literal(fromModule)
|
|
183
|
+
);
|
|
184
|
+
root.find(j.Program).get('body', 0).insertBefore(importStatement);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export function removeCSSImport(root, j) {
|
|
189
|
+
root.find(j.ImportDeclaration).forEach((path) => {
|
|
190
|
+
if (/\.css$/.test(path.node.source.value)) {
|
|
191
|
+
j(path).remove();
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function getCSSImportPath(root, j) {
|
|
197
|
+
let cssPath = null;
|
|
198
|
+
|
|
199
|
+
root.find(j.ImportDeclaration).forEach((path) => {
|
|
200
|
+
const isCSSImport = /\.css$/.test(path.node.source.value);
|
|
201
|
+
if (isCSSImport) {
|
|
202
|
+
cssPath = path.node.source.value;
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
return cssPath;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function addReactImportIfMissing(root, j) {
|
|
210
|
+
const hasReactImport = root
|
|
211
|
+
.find(j.ImportDeclaration, {
|
|
212
|
+
source: { value: 'react' },
|
|
213
|
+
})
|
|
214
|
+
.size();
|
|
215
|
+
|
|
216
|
+
if (!hasReactImport) {
|
|
217
|
+
const importStatement = j.importDeclaration([], j.literal('react'));
|
|
218
|
+
root.find(j.Program).get('body', 0).insertBefore(importStatement);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function getTemplateLiteralValue(node) {
|
|
223
|
+
if (node.type !== 'TemplateLiteral') {
|
|
224
|
+
throw new Error('Node is not a TemplateLiteral');
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
let value = '';
|
|
228
|
+
|
|
229
|
+
// Loop through each quasi and expression, and append them to the value string
|
|
230
|
+
for (let i = 0; i < node.quasis.length; i++) {
|
|
231
|
+
value += node.quasis[i].value.raw; // Get the raw value of the quasi
|
|
232
|
+
|
|
233
|
+
// If there's an expression after this quasi, append its value too
|
|
234
|
+
if (i < node.expressions.length) {
|
|
235
|
+
value += node.expressions[i].name; // Assuming the expression is an Identifier for simplicity
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return value;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function transformCode(root, j, cssClassStyles, assetsDir, assetsUrl) {
|
|
243
|
+
// eslint-disable-next-line prefer-const
|
|
244
|
+
let MapComponent = {};
|
|
245
|
+
|
|
246
|
+
function processPropValue(prop, value, className) {
|
|
247
|
+
const numericalMatch = value.match(/^\d+(\.\d+)?(px)?$/);
|
|
248
|
+
|
|
249
|
+
if (numericalMatch) {
|
|
250
|
+
const number = numericalMatch[0].replace('px', ''); // Remove 'px' if present
|
|
251
|
+
if (number.indexOf('.') >= 0) {
|
|
252
|
+
return parseFloat(number);
|
|
253
|
+
} else {
|
|
254
|
+
return parseInt(number, 10);
|
|
255
|
+
}
|
|
256
|
+
} else if (prop === 'boxShadow') {
|
|
257
|
+
const matches = /(\d+)px (\d+)px (\d+)px rgba\(([\d,]+),([\d.]+)\)/.exec(
|
|
258
|
+
value
|
|
259
|
+
);
|
|
260
|
+
if (matches) {
|
|
261
|
+
const [, height, width, radius, shadowColor, opacity] = matches;
|
|
262
|
+
return {
|
|
263
|
+
shadow: {
|
|
264
|
+
shadowOffset: { height: parseInt(height), width: parseInt(width) },
|
|
265
|
+
shadowRadius: parseInt(radius),
|
|
266
|
+
shadowColor: shadowColor.split(',').map(Number),
|
|
267
|
+
shadowOpacity: parseFloat(opacity),
|
|
268
|
+
},
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
} else if (value.indexOf('url(') >= 0) {
|
|
272
|
+
const parts = value.split('url("data:image/');
|
|
273
|
+
if (parts.length > 1) {
|
|
274
|
+
const [imageTypeAndEncoding, dataWithQuotes] = parts[1].split(',');
|
|
275
|
+
const imageType = imageTypeAndEncoding.split(';')[0].split('+')[0];
|
|
276
|
+
const data = dataWithQuotes.slice(0, -2); // Remove trailing '")'
|
|
277
|
+
|
|
278
|
+
const filename = `${className}.${imageType}`;
|
|
279
|
+
|
|
280
|
+
// Decoding the base64 content and writing as binary
|
|
281
|
+
if (value.indexOf('base64') > 0) {
|
|
282
|
+
const buffer = Buffer.from(data, 'base64');
|
|
283
|
+
fs.writeFileSync(path.join(assetsDir, filename), buffer);
|
|
284
|
+
} else {
|
|
285
|
+
fs.writeFileSync(
|
|
286
|
+
path.join(assetsDir, filename),
|
|
287
|
+
decodeURIComponent(data)
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return `url("${assetsUrl}/${filename}")`;
|
|
292
|
+
}
|
|
293
|
+
} else {
|
|
294
|
+
return value;
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
root.find(j.JSXElement).forEach((path) => {
|
|
299
|
+
const tagName = path.node.openingElement.name.name;
|
|
300
|
+
// eslint-disable-next-line prefer-const
|
|
301
|
+
let mappedComponent = COMPONENT_MAPPING[tagName] || APP_MAPPING[tagName];
|
|
302
|
+
|
|
303
|
+
if (APP_MAPPING[tagName]) IMPORT_APP[mappedComponent] = true;
|
|
304
|
+
else if (COMPONENT_MAPPING[tagName])
|
|
305
|
+
IMPORT_COMPONENT[mappedComponent] = true;
|
|
306
|
+
else IMPORT_APP['View'] = true;
|
|
307
|
+
|
|
308
|
+
const attributes = path.node.openingElement.attributes;
|
|
309
|
+
let classNames = [];
|
|
310
|
+
|
|
311
|
+
for (const attribute of attributes) {
|
|
312
|
+
if (
|
|
313
|
+
attribute.type === 'JSXAttribute' &&
|
|
314
|
+
attribute.name.name === 'className'
|
|
315
|
+
) {
|
|
316
|
+
const classNameString = getTemplateLiteralValue(
|
|
317
|
+
attribute.value.expression
|
|
318
|
+
);
|
|
319
|
+
classNames = classNameString.split(' '); // Assuming class names are separated by spaces
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const combinedProps = classNames.reduce((acc, className) => {
|
|
324
|
+
const props = cssClassStyles[className];
|
|
325
|
+
if (props) {
|
|
326
|
+
const processedProps = {};
|
|
327
|
+
|
|
328
|
+
for (const [key, value] of Object.entries(props)) {
|
|
329
|
+
processedProps[key] = processPropValue(key, value, className);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return { ...acc, ...processedProps };
|
|
333
|
+
}
|
|
334
|
+
return acc;
|
|
335
|
+
}, {});
|
|
336
|
+
|
|
337
|
+
const combinedComponentName = classNames
|
|
338
|
+
.map((className) =>
|
|
339
|
+
className
|
|
340
|
+
.match(/\w+/g)
|
|
341
|
+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
|
342
|
+
.join('')
|
|
343
|
+
)
|
|
344
|
+
.join('');
|
|
345
|
+
|
|
346
|
+
MapComponent[combinedComponentName] = mappedComponent || 'View';
|
|
347
|
+
|
|
348
|
+
path.node.openingElement.attributes =
|
|
349
|
+
path.node.openingElement.attributes.filter(
|
|
350
|
+
(attribute) => attribute.name.name !== 'className'
|
|
351
|
+
);
|
|
352
|
+
|
|
353
|
+
if (combinedComponentName) {
|
|
354
|
+
path.node.openingElement.name.name = combinedComponentName;
|
|
355
|
+
if (path.node.closingElement)
|
|
356
|
+
path.node.closingElement.name.name = combinedComponentName;
|
|
357
|
+
} else if (tagName && isHtmlElement(tagName)) {
|
|
358
|
+
const asAttribute = j.jsxAttribute(
|
|
359
|
+
j.jsxIdentifier('as'),
|
|
360
|
+
j.stringLiteral(tagName)
|
|
361
|
+
);
|
|
362
|
+
path.node.openingElement.name.name = 'View';
|
|
363
|
+
if (path.node.closingElement) path.node.closingElement.name.name = 'View';
|
|
364
|
+
path.node.openingElement.attributes.push(asAttribute);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const props = Object.entries(combinedProps).map(([key, value]) => {
|
|
368
|
+
let valueLiteral;
|
|
369
|
+
if (typeof value === 'object' && value !== null) {
|
|
370
|
+
valueLiteral = j.objectExpression(
|
|
371
|
+
Object.entries(value).map(([innerKey, innerValue]) =>
|
|
372
|
+
j.objectProperty(j.identifier(innerKey), j.literal(innerValue))
|
|
373
|
+
)
|
|
374
|
+
);
|
|
375
|
+
} else if (typeof value === 'string') {
|
|
376
|
+
valueLiteral = j.literal(value);
|
|
377
|
+
} else {
|
|
378
|
+
valueLiteral = j.numericLiteral(value);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return j.jsxAttribute(
|
|
382
|
+
j.jsxIdentifier(key),
|
|
383
|
+
j.jsxExpressionContainer(valueLiteral)
|
|
384
|
+
);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
const newComponent = j.variableDeclaration('const', [
|
|
388
|
+
j.variableDeclarator(
|
|
389
|
+
j.identifier(combinedComponentName),
|
|
390
|
+
j.arrowFunctionExpression(
|
|
391
|
+
[j.identifier('props')],
|
|
392
|
+
j.jsxElement(
|
|
393
|
+
j.jsxOpeningElement(
|
|
394
|
+
j.jsxIdentifier(MapComponent[combinedComponentName] || 'View'),
|
|
395
|
+
[...props, j.jsxSpreadAttribute(j.identifier('props'))],
|
|
396
|
+
true
|
|
397
|
+
),
|
|
398
|
+
null
|
|
399
|
+
)
|
|
400
|
+
)
|
|
401
|
+
),
|
|
402
|
+
]);
|
|
403
|
+
|
|
404
|
+
root.find(j.Program).forEach((path) => {
|
|
405
|
+
path.node.body.push(newComponent);
|
|
406
|
+
});
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export default function transform(file, api, options) {
|
|
411
|
+
const j = api.jscodeshift;
|
|
412
|
+
const root = j(file.source);
|
|
413
|
+
|
|
414
|
+
const assetsDir = options.assetsDir || 'src/assets';
|
|
415
|
+
const assetsUrl = options.assetsUrl || '/assets';
|
|
416
|
+
|
|
417
|
+
// eslint-disable-next-line prefer-const
|
|
418
|
+
let imports = {};
|
|
419
|
+
|
|
420
|
+
const cssImportPath = getCSSImportPath(root, j);
|
|
421
|
+
let cssContent = '';
|
|
422
|
+
|
|
423
|
+
if (cssImportPath) {
|
|
424
|
+
const cssFilePath = path.resolve(path.dirname(file.path), cssImportPath);
|
|
425
|
+
try {
|
|
426
|
+
cssContent = fs.readFileSync(cssFilePath, 'utf8');
|
|
427
|
+
} catch (err) {
|
|
428
|
+
console.error('Failed to read CSS file:', err);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const cssClassStyles = {};
|
|
433
|
+
|
|
434
|
+
// Parse CSS content using postcss
|
|
435
|
+
const rootCss = postcss.parse(cssContent);
|
|
436
|
+
|
|
437
|
+
rootCss.walkRules((rule) => {
|
|
438
|
+
if (rule.selector.startsWith('.')) {
|
|
439
|
+
const className = rule.selector.slice(1);
|
|
440
|
+
const styles = {};
|
|
441
|
+
|
|
442
|
+
rule.walkDecls((decl) => {
|
|
443
|
+
const key = decl.prop;
|
|
444
|
+
const value = decl.value;
|
|
445
|
+
const propName = key.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
|
|
446
|
+
styles[propName] = value;
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
cssClassStyles[className] = styles;
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
transformCode(root, j, cssClassStyles, assetsDir, assetsUrl);
|
|
454
|
+
addReactImportIfMissing(root, j);
|
|
455
|
+
|
|
456
|
+
transformStyledComponentsToView(root, j, imports);
|
|
457
|
+
transformStyleToProps(root, j, imports);
|
|
458
|
+
|
|
459
|
+
removeCSSImport(root, j);
|
|
460
|
+
|
|
461
|
+
const componentImport = Object.keys(IMPORT_COMPONENT);
|
|
462
|
+
|
|
463
|
+
const appImport = Object.keys(IMPORT_APP);
|
|
464
|
+
|
|
465
|
+
for (const componentName in imports) {
|
|
466
|
+
if (
|
|
467
|
+
APP_MAPPING[componentName] !== undefined &&
|
|
468
|
+
!appImport.includes(componentName)
|
|
469
|
+
) {
|
|
470
|
+
appImport.push(componentName);
|
|
471
|
+
} else if (
|
|
472
|
+
COMPONENT_MAPPING[componentName] !== undefined &&
|
|
473
|
+
!componentImport.includes(componentName)
|
|
474
|
+
) {
|
|
475
|
+
componentImport.push(componentName);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (componentImport.length > 0)
|
|
480
|
+
addImportStatement(root, j, componentImport, '@app-studio/web');
|
|
481
|
+
|
|
482
|
+
if (appImport.length > 0)
|
|
483
|
+
addImportStatement(root, j, appImport, 'app-studio');
|
|
484
|
+
|
|
485
|
+
return root.toSource();
|
|
486
|
+
}
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
export const APP_MAPPING = {
|
|
2
|
+
A: 'a',
|
|
3
|
+
Image: 'img',
|
|
4
|
+
Div: 'div',
|
|
5
|
+
Span: 'span',
|
|
6
|
+
View: 'div',
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const COMPONENT_MAPPING = {
|
|
10
|
+
Textarea: 'textarea',
|
|
11
|
+
Svg: 'svg',
|
|
12
|
+
Select: 'select',
|
|
13
|
+
Image: 'picture',
|
|
14
|
+
Option: 'option',
|
|
15
|
+
Map: 'map',
|
|
16
|
+
Input: 'input',
|
|
17
|
+
Iframe: 'iframe',
|
|
18
|
+
Form: 'form',
|
|
19
|
+
Button: 'button',
|
|
20
|
+
Audio: 'audio',
|
|
21
|
+
Video: 'video',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function transformHTMLToView(root, j, imports) {
|
|
25
|
+
root.find(j.JSXElement).forEach((path) => {
|
|
26
|
+
const tagName = path.node.openingElement.name.name;
|
|
27
|
+
const mappedComponent = COMPONENT_MAPPING[tagName];
|
|
28
|
+
|
|
29
|
+
if (mappedComponent) {
|
|
30
|
+
path.node.openingElement.name.name = mappedComponent;
|
|
31
|
+
if (path.node.closingElement)
|
|
32
|
+
path.node.closingElement.name.name = mappedComponent;
|
|
33
|
+
} else {
|
|
34
|
+
console.log({ tagName });
|
|
35
|
+
if (tagName && isHtmlElement(tagName)) {
|
|
36
|
+
const asAttribute = j.jsxAttribute(
|
|
37
|
+
j.jsxIdentifier('as'),
|
|
38
|
+
j.stringLiteral(tagName)
|
|
39
|
+
);
|
|
40
|
+
console.log({ name: path.node.openingElement.name.name });
|
|
41
|
+
|
|
42
|
+
path.node.openingElement.name.name = 'View';
|
|
43
|
+
if (path.node.closingElement)
|
|
44
|
+
path.node.closingElement.name.name = 'View';
|
|
45
|
+
path.node.openingElement.attributes.push(asAttribute);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isHtmlElement(elementName) {
|
|
52
|
+
return elementName[0] === elementName[0].toLowerCase();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function transformStyledComponentsToView(root, j, imports) {
|
|
56
|
+
root
|
|
57
|
+
.find(j.TaggedTemplateExpression)
|
|
58
|
+
.filter((path) => {
|
|
59
|
+
return (
|
|
60
|
+
path.node.tag.type === 'Identifier' && path.node.tag.name === 'styled'
|
|
61
|
+
);
|
|
62
|
+
})
|
|
63
|
+
.forEach((path) => {
|
|
64
|
+
const quasis = path.node.quasi.quasis;
|
|
65
|
+
|
|
66
|
+
// eslint-disable-next-line prefer-const
|
|
67
|
+
let mediaQueries = {};
|
|
68
|
+
let rootStyles = {};
|
|
69
|
+
|
|
70
|
+
quasis.forEach((quasi, index) => {
|
|
71
|
+
const cssString = quasi.value.raw;
|
|
72
|
+
|
|
73
|
+
// Extrait les media queries
|
|
74
|
+
const mediaRegex =
|
|
75
|
+
/@media \(?(min-width:\s*(\d+)px)?\s*(and)?\s*(max-width:\s*(\d+)px)?\)?\s*\{([\s\S]*?)\}/g;
|
|
76
|
+
let match;
|
|
77
|
+
|
|
78
|
+
while ((match = mediaRegex.exec(cssString)) !== null) {
|
|
79
|
+
const minWidth = match[2] ? parseInt(match[2], 10) : null;
|
|
80
|
+
const maxWidth = match[5] ? parseInt(match[5], 10) : null;
|
|
81
|
+
const stylesString = match[6].trim();
|
|
82
|
+
|
|
83
|
+
let breakpointName = 'xs';
|
|
84
|
+
|
|
85
|
+
if (minWidth) {
|
|
86
|
+
breakpointName += `min[${minWidth}]`;
|
|
87
|
+
}
|
|
88
|
+
if (maxWidth) {
|
|
89
|
+
breakpointName += `max[${maxWidth}]`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const styles = stylesString
|
|
93
|
+
.split(';')
|
|
94
|
+
.filter(Boolean)
|
|
95
|
+
.reduce((acc, style) => {
|
|
96
|
+
const [key, value] = style.split(':').map((str) => str.trim());
|
|
97
|
+
if (key && value) {
|
|
98
|
+
acc[key] = value;
|
|
99
|
+
}
|
|
100
|
+
return acc;
|
|
101
|
+
}, {});
|
|
102
|
+
|
|
103
|
+
mediaQueries[breakpointName] = styles;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Extrait les styles root
|
|
107
|
+
const rootRegex = /([^@{}]+)\{([\s\S]*?)\}/g;
|
|
108
|
+
while ((match = rootRegex.exec(cssString)) !== null) {
|
|
109
|
+
const stylesString = match[2].trim();
|
|
110
|
+
const styles = stylesString
|
|
111
|
+
.split(';')
|
|
112
|
+
.filter(Boolean)
|
|
113
|
+
.reduce((acc, style) => {
|
|
114
|
+
const [key, value] = style.split(':').map((str) => str.trim());
|
|
115
|
+
if (key && value) {
|
|
116
|
+
acc[key] = value;
|
|
117
|
+
}
|
|
118
|
+
return acc;
|
|
119
|
+
}, {});
|
|
120
|
+
rootStyles = { ...rootStyles, ...styles };
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
let attributes = [j.jsxSpreadAttribute(j.identifier('props'))];
|
|
125
|
+
|
|
126
|
+
if (Object.keys(mediaQueries).length > 0) {
|
|
127
|
+
const mediaProp = j.jsxAttribute(
|
|
128
|
+
j.jsxIdentifier('media')
|
|
129
|
+
// ... (le même code pour créer la prop 'media') ...
|
|
130
|
+
);
|
|
131
|
+
attributes.push(mediaProp);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (Object.keys(rootStyles).length > 0) {
|
|
135
|
+
const rootStyleProps = Object.keys(rootStyles).map((key) =>
|
|
136
|
+
j.jsxAttribute(j.jsxIdentifier(key), j.literal(rootStyles[key]))
|
|
137
|
+
);
|
|
138
|
+
attributes = [...attributes, ...rootStyleProps];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
path.node.init = j.arrowFunctionExpression(
|
|
142
|
+
[j.identifier('props')],
|
|
143
|
+
j.jsxElement(
|
|
144
|
+
j.jsxOpeningElement(j.jsxIdentifier('View'), attributes),
|
|
145
|
+
j.jsxClosingElement(j.jsxIdentifier('View')),
|
|
146
|
+
[]
|
|
147
|
+
)
|
|
148
|
+
);
|
|
149
|
+
imports['View'] = true;
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export function transformStyleToProps(root, j, imports) {
|
|
154
|
+
root.find(j.JSXAttribute, { name: { name: 'style' } }).forEach((path) => {
|
|
155
|
+
const attrValue = path.node.value;
|
|
156
|
+
// eslint-disable-next-line prefer-const
|
|
157
|
+
let newAttributes = [];
|
|
158
|
+
|
|
159
|
+
if (attrValue.type === 'Literal') {
|
|
160
|
+
const inlineStyles = attrValue.value.split(';').filter(Boolean);
|
|
161
|
+
inlineStyles.forEach((inlineStyle) => {
|
|
162
|
+
const [key, value] = inlineStyle.split(':').map((s) => s.trim());
|
|
163
|
+
const propName = key.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
|
|
164
|
+
newAttributes.push(
|
|
165
|
+
j.jsxAttribute(j.jsxIdentifier(propName), j.stringLiteral(value))
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
} else if (
|
|
169
|
+
attrValue.type === 'JSXExpressionContainer' &&
|
|
170
|
+
attrValue.expression.type === 'ObjectExpression'
|
|
171
|
+
) {
|
|
172
|
+
attrValue.expression.properties.forEach((prop) => {
|
|
173
|
+
const propName = prop.key.name;
|
|
174
|
+
const propValue = prop.value;
|
|
175
|
+
newAttributes.push(
|
|
176
|
+
j.jsxAttribute(j.jsxIdentifier(propName), propValue)
|
|
177
|
+
);
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const openingElement = path.parentPath.node;
|
|
182
|
+
openingElement.attributes = openingElement.attributes.filter(
|
|
183
|
+
(attr) => attr.name.name !== 'style'
|
|
184
|
+
);
|
|
185
|
+
openingElement.attributes.push(...newAttributes);
|
|
186
|
+
imports['View'] = true;
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Fonction pour ajouter une déclaration d'importation si elle n'est pas déjà présente
|
|
191
|
+
export function addImportStatement(root, j, importName, fromModule) {
|
|
192
|
+
const existingImport = root.find(j.ImportDeclaration, {
|
|
193
|
+
source: { value: fromModule },
|
|
194
|
+
});
|
|
195
|
+
if (existingImport.size() === 0) {
|
|
196
|
+
const importStatement = j.importDeclaration(
|
|
197
|
+
[j.importSpecifier(j.identifier(importName))],
|
|
198
|
+
j.literal(fromModule)
|
|
199
|
+
);
|
|
200
|
+
root.find(j.Program).get('body', 0).insertBefore(importStatement);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function mapCSSClassToProps(root, j, cssContent) {
|
|
205
|
+
// Use a simple regex to extract class names and their styles
|
|
206
|
+
const cssClassRegex = /\.([a-zA-Z0-9-_]+)\s*\{([\s\S]*?)\}/g;
|
|
207
|
+
let cssClassMatch;
|
|
208
|
+
|
|
209
|
+
const cssClassStyles = {};
|
|
210
|
+
|
|
211
|
+
while ((cssClassMatch = cssClassRegex.exec(cssContent)) !== null) {
|
|
212
|
+
const className = cssClassMatch[1];
|
|
213
|
+
const styles = cssClassMatch[2]
|
|
214
|
+
.split(';')
|
|
215
|
+
.filter(Boolean)
|
|
216
|
+
.reduce((acc, style) => {
|
|
217
|
+
const [key, value] = style.split(':').map((str) => str.trim());
|
|
218
|
+
if (key && value) {
|
|
219
|
+
acc[key] = value;
|
|
220
|
+
}
|
|
221
|
+
return acc;
|
|
222
|
+
}, {});
|
|
223
|
+
|
|
224
|
+
cssClassStyles[className] = styles;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Iterate over JSXElements and check for className attributes
|
|
228
|
+
root.find(j.JSXElement).forEach((path) => {
|
|
229
|
+
const classNameAttribute = path.node.openingElement.attributes.find(
|
|
230
|
+
(attr) => attr.name && attr.name.name === 'className'
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
if (classNameAttribute) {
|
|
234
|
+
const classNameValue = classNameAttribute.value.value;
|
|
235
|
+
const mappedStyles = cssClassStyles[classNameValue];
|
|
236
|
+
if (mappedStyles) {
|
|
237
|
+
const newAttributes = Object.keys(mappedStyles).map((key) => {
|
|
238
|
+
const propName = key.replace(/-([a-z])/g, (g) => g[1].toUpperCase());
|
|
239
|
+
return j.jsxAttribute(
|
|
240
|
+
j.jsxIdentifier(propName),
|
|
241
|
+
j.stringLiteral(mappedStyles[key])
|
|
242
|
+
);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
path.node.openingElement.attributes = [
|
|
246
|
+
...path.node.openingElement.attributes.filter(
|
|
247
|
+
(attr) => attr.name.name !== 'className'
|
|
248
|
+
),
|
|
249
|
+
...newAttributes,
|
|
250
|
+
];
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function getCSSImportPath(root, j) {
|
|
257
|
+
let cssPath = null;
|
|
258
|
+
|
|
259
|
+
root.find(j.ImportDeclaration).forEach((path) => {
|
|
260
|
+
const isCSSImport = /\.css$/.test(path.node.source.value);
|
|
261
|
+
if (isCSSImport) {
|
|
262
|
+
cssPath = path.node.source.value;
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
return cssPath;
|
|
267
|
+
}
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.1.
|
|
2
|
+
"version": "0.1.19",
|
|
3
3
|
"name": "app-studio",
|
|
4
4
|
"description": "App Studio is a responsive and themeable framework to build cross platform applications",
|
|
5
5
|
"repository": "git@github.com:rize-network/app-studio.git",
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"responsive"
|
|
19
19
|
],
|
|
20
20
|
"files": [
|
|
21
|
-
"dist"
|
|
21
|
+
"dist",
|
|
22
|
+
"codemod"
|
|
22
23
|
],
|
|
23
24
|
"engines": {
|
|
24
25
|
"node": ">=10"
|