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.
@@ -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
+ }
@@ -0,0 +1,12 @@
1
+ {
2
+ "compilerOptions": {
3
+ "module": "commonjs",
4
+ "sourceMap": true,
5
+ "esModuleInterop": true,
6
+ "target": "es2015",
7
+ "downlevelIteration": true,
8
+ "preserveWatchOutput": true
9
+ },
10
+ "include": ["**/*.ts"],
11
+ "exclude": ["node_modules"]
12
+ }
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.1.18",
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"