[]) {
93 | let textWithArgs = '';
94 | let textWithoutArgs = '';
95 | let argIndex = 0;
96 | let hasText = false;
97 | const texts = [];
98 | for(let i = 0; i < jsxContentNodes.length; i++) {
99 | const element = jsxContentNodes[i];
100 | if (element.type === 'JSXText') {
101 | hasText = true;
102 | textWithArgs += element.value;
103 | textWithoutArgs += element.value;
104 | } else if (element.type === 'JSXExpressionContainer') {
105 | textWithArgs += `{arg${argIndex}}`;
106 | argIndex++;
107 | } else {
108 | if (hasText) {
109 | texts.push({ textWithArgs, textWithoutArgs });
110 | textWithArgs = '';
111 | textWithoutArgs = ''
112 | }
113 | }
114 | }
115 | if (hasText) {
116 | texts.push({ textWithArgs, textWithoutArgs });
117 | }
118 | return texts;
119 | }
120 |
121 | BabelPluginI18n.clear = () => {
122 | phrases = [];
123 | i18nMap = {};
124 | };
125 | BabelPluginI18n.setMaxKeyLength = (maxLength: number) => {
126 | keyMaxLength = maxLength;
127 | };
128 | BabelPluginI18n.getExtractedStrings = () => phrases;
129 | BabelPluginI18n.getI18Map = () => i18nMap;
130 |
131 | export default BabelPluginI18n;
132 |
--------------------------------------------------------------------------------
/src/__test__/filterFiles.test.ts:
--------------------------------------------------------------------------------
1 | import { filterFiles, DEFAULT_TEST_FILE_REGEX } from "../filterFiles";
2 |
3 | const mockFilesPath = [
4 | "src/__test__/filterFiles.test.ts",
5 | "src/__test__/generateResources.spec.ts",
6 | "src/__testfixtures__/CallExpression.input.tsx",
7 | "src/__testfixtures__/CallExpression.output.tsx",
8 | "src/userArgv.js",
9 | "src/visitorChecks.ts",
10 | "src/run.ex",
11 | ];
12 |
13 | const shellFindMock = () => mockFilesPath;
14 | const filterFilesWithShell = filterFiles(shellFindMock);
15 |
16 | describe("filterFiles", () => {
17 | it(`should receive a path and return a list of files with (js|ts|tsx) extension and ignore files that match with ${DEFAULT_TEST_FILE_REGEX}`, () => {
18 | const files = filterFilesWithShell("./src");
19 |
20 | expect(files).toStrictEqual([
21 | "src/__testfixtures__/CallExpression.input.tsx",
22 | "src/__testfixtures__/CallExpression.output.tsx",
23 | "src/userArgv.js",
24 | "src/visitorChecks.ts",
25 | ]);
26 | });
27 |
28 | it("should receive a path, a custom regex and return a list of files with (js|ts|tsx) extension and ignore the files that match with regex", () => {
29 | const files = filterFilesWithShell("./src", "/(__testfixtures__|__test__)/");
30 |
31 | expect(files).toStrictEqual([
32 | "src/userArgv.js",
33 | "src/visitorChecks.ts",
34 | ]);
35 | });
36 | });
37 |
--------------------------------------------------------------------------------
/src/__test__/generateResources.spec.ts:
--------------------------------------------------------------------------------
1 | import path from "path";
2 |
3 | import { generateResources, getResourceSource } from '../generateResources';
4 | import fs from "fs";
5 | import BabelPluginI18n from '../BabelPluginI18n';
6 |
7 | const defineTest = (dirName: string, testFilePrefix: string, only: boolean = false) => {
8 | const testName = `extra string from ${testFilePrefix}`;
9 |
10 | const myIt = only ? it.only : it;
11 |
12 | myIt(testName, () => {
13 | const fixtureDir = path.join(dirName, '..', '__testfixtures__');
14 | const inputPath = path.join(fixtureDir, testFilePrefix + '.input.tsx');
15 | const expectedOutput = fs.readFileSync(
16 | path.join(fixtureDir, testFilePrefix + '.resource.tsx'),
17 | 'utf8'
18 | );
19 |
20 | const files = [inputPath];
21 | const i18nMap = generateResources(files);
22 |
23 | expect(getResourceSource(i18nMap).trim()).toEqual(expectedOutput.trim());
24 | });
25 | };
26 |
27 | describe('generateResources', () => {
28 | beforeEach(() => {
29 | BabelPluginI18n.clear();
30 | });
31 |
32 | defineTest(__dirname, 'Classes');
33 | defineTest(__dirname, 'Diacritics');
34 | defineTest(__dirname, 'ExpressionContainer');
35 | defineTest(__dirname, 'Functional');
36 | defineTest(__dirname, 'Props');
37 | defineTest(__dirname, 'Tsx');
38 | defineTest(__dirname, 'Yup');
39 | defineTest(__dirname, 'CallExpression');
40 | defineTest(__dirname, 'Svg');
41 | defineTest(__dirname, 'Parameters');
42 | });
43 |
--------------------------------------------------------------------------------
/src/__test__/i18nTransfomerCodemod.test.ts:
--------------------------------------------------------------------------------
1 | import { defineTest } from '../../test/testUtils';
2 |
3 | describe('i18nTransformerCodemod', () => {
4 | defineTest(__dirname, 'i18nTransformerCodemod', null, 'Classes');
5 | defineTest(__dirname, 'i18nTransformerCodemod', null, 'Diacritics');
6 | defineTest(__dirname, 'i18nTransformerCodemod', null, 'ExpressionContainer');
7 | defineTest(__dirname, 'i18nTransformerCodemod', null, 'Functional');
8 | defineTest(__dirname, 'i18nTransformerCodemod', null, 'Props');
9 | defineTest(__dirname, 'i18nTransformerCodemod', null, 'Tsx');
10 | defineTest(__dirname, 'i18nTransformerCodemod', null, 'Yup');
11 | defineTest(__dirname, 'i18nTransformerCodemod', null, 'CallExpression');
12 | defineTest(__dirname, 'i18nTransformerCodemod', null, 'Imported');
13 | defineTest(__dirname, 'i18nTransformerCodemod', null, 'WithHoc');
14 | defineTest(__dirname, 'i18nTransformerCodemod', null, 'NoChange');
15 | defineTest(__dirname, 'i18nTransformerCodemod', null, 'Hooks');
16 | defineTest(__dirname, 'i18nTransformerCodemod', null, 'HooksInlineExport');
17 | defineTest(__dirname, 'i18nTransformerCodemod', null, 'Classnames');
18 | defineTest(__dirname, 'i18nTransformerCodemod', null, 'Svg');
19 | defineTest(__dirname, 'i18nTransformerCodemod', null, 'Parameters');
20 | });
21 |
--------------------------------------------------------------------------------
/src/__test__/stableString.test.ts:
--------------------------------------------------------------------------------
1 | import { getStableKey } from '../stableString';
2 |
3 | it('should handle convert to lowercase', () => {
4 | expect(getStableKey('AWESOME')).toBe('awesome');
5 | });
6 |
7 | it('should transform white space to underscore', () => {
8 | expect(getStableKey('AWESOME wOrk')).toBe('awesome_work');
9 | });
10 |
11 | it('should not add underscore because of start white space', () => {
12 | expect(getStableKey(' AWESOME wOrk')).toBe('awesome_work');
13 | });
14 |
15 | it('should not add underscore because of end white space', () => {
16 | expect(getStableKey(' AWESOME wOrk ')).toBe('awesome_work');
17 | });
18 |
19 | it('should not add multiple underscores because of multiple white spaces', () => {
20 | expect(getStableKey(' AWESOME wOrk ')).toBe('awesome_work');
21 | });
22 |
23 | it('should remove diacrits', () => {
24 | expect(getStableKey('Olá Brasil')).toBe('ola_brasil');
25 | });
26 |
27 | // TODO - figure it out how to remove this ×
28 | it('handle weird chars', () => {
29 | expect(getStableKey('×_fechar')).toBe('_fechar');
30 | });
31 |
--------------------------------------------------------------------------------
/src/__testfixtures__/CallExpression.input.tsx:
--------------------------------------------------------------------------------
1 | const callIt = ({ showSnackbar }) => {
2 | showSnackbar({ message: 'User editted successfully!'});
3 | };
4 |
5 | export default callIt;
6 |
--------------------------------------------------------------------------------
/src/__testfixtures__/CallExpression.output.tsx:
--------------------------------------------------------------------------------
1 | import { useTranslation } from 'react-i18next';
2 | const callIt = ({ showSnackbar }) => {
3 | const { t } = useTranslation();
4 | showSnackbar({ message: t('user_editted_successfully')});
5 | };
6 |
7 | export default callIt;
8 |
--------------------------------------------------------------------------------
/src/__testfixtures__/CallExpression.resource.tsx:
--------------------------------------------------------------------------------
1 | export default {
2 | translation: {
3 | user_editted_successfully: `User editted successfully!`,
4 | },
5 | };
6 |
--------------------------------------------------------------------------------
/src/__testfixtures__/Classes.input.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | class MyClass extends React.Component {
4 | render() {
5 | return (
6 |
7 | my great class component
8 |
9 | );
10 | }
11 | }
12 |
13 | export default MyClass;
14 |
--------------------------------------------------------------------------------
/src/__testfixtures__/Classes.output.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { withTranslation } from 'react-i18next';
4 |
5 | class MyClass extends React.Component {
6 | render() {
7 | const { t } = this.props;
8 | return (
9 |
10 | {t('my_great_class_component')}
11 |
12 | );
13 | }
14 | }
15 |
16 | export default withTranslation()(MyClass);
17 |
--------------------------------------------------------------------------------
/src/__testfixtures__/Classes.resource.tsx:
--------------------------------------------------------------------------------
1 | export default {
2 | translation: {
3 | my_great_class_component: `my great class component`,
4 | },
5 | };
6 |
--------------------------------------------------------------------------------
/src/__testfixtures__/Classnames.input.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import classNames from "classnames";
3 |
4 | function Button({ isPrimary, title }) {
5 | const className = classNames("special-button", {
6 | "special-button--primary": isPrimary
7 | });
8 |
9 | return ;
10 | }
11 |
12 | export default Button;
13 |
--------------------------------------------------------------------------------
/src/__testfixtures__/Classnames.output.tsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sibelius/ast-i18n/f5ac4c365dd5279d77685a7a966b574c95d7ac80/src/__testfixtures__/Classnames.output.tsx
--------------------------------------------------------------------------------
/src/__testfixtures__/Diacritics.input.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | const Simple = () => (
4 | Olá Antônio
5 | );
6 |
7 | export default Simple;
8 |
--------------------------------------------------------------------------------
/src/__testfixtures__/Diacritics.output.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { useTranslation } from 'react-i18next';
4 |
5 | const Simple = () => {
6 | const { t } = useTranslation();
7 | return {t('ola_antonio')};
8 | };
9 |
10 | export default Simple;
--------------------------------------------------------------------------------
/src/__testfixtures__/Diacritics.resource.tsx:
--------------------------------------------------------------------------------
1 | export default {
2 | translation: {
3 | ola_antonio: `Olá Antônio`,
4 | },
5 | };
6 |
--------------------------------------------------------------------------------
/src/__testfixtures__/ExpressionContainer.input.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const Simple = ({enabled, text}) => (
4 |
5 | {"My simple text"}
6 | {enabled ? "OK" : "Not OK"}
7 | {text && text}
8 |
9 | );
10 |
11 | export default Simple;
--------------------------------------------------------------------------------
/src/__testfixtures__/ExpressionContainer.output.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { useTranslation } from 'react-i18next';
4 |
5 | const Simple = ({enabled, text}) => {
6 | const { t } = useTranslation();
7 |
8 | return (
9 |
10 | {t('my_simple_text')}
11 | {enabled ? t('ok') : t('not_ok')}
12 | {text && text}
13 |
14 | );
15 | };
16 |
17 | export default Simple;
--------------------------------------------------------------------------------
/src/__testfixtures__/ExpressionContainer.resource.tsx:
--------------------------------------------------------------------------------
1 | export default {
2 | translation: {
3 | my_simple_text: `My simple text`,
4 | ok: `OK`,
5 | not_ok: `Not OK`,
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/src/__testfixtures__/Functional.input.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function Simple() {
4 | return My simple text;
5 | }
6 |
7 | export default Simple;
8 |
--------------------------------------------------------------------------------
/src/__testfixtures__/Functional.output.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { useTranslation } from 'react-i18next';
4 |
5 | function Simple() {
6 | const { t } = useTranslation();
7 | return {t('my_simple_text')};
8 | }
9 |
10 | export default Simple;
--------------------------------------------------------------------------------
/src/__testfixtures__/Functional.resource.tsx:
--------------------------------------------------------------------------------
1 | export default {
2 | translation: {
3 | my_simple_text: `My simple text`,
4 | },
5 | };
6 |
--------------------------------------------------------------------------------
/src/__testfixtures__/Hooks.input.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | const SiteHeader = () => {
4 | const [text] = useState('');
5 | return (
6 | My simple text
7 | );
8 | };
9 |
10 | export default SiteHeader;
11 |
--------------------------------------------------------------------------------
/src/__testfixtures__/Hooks.output.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | import { useTranslation } from 'react-i18next';
4 |
5 | const SiteHeader = () => {
6 | const { t } = useTranslation();
7 | const [text] = useState('');
8 | return {t('my_simple_text')};
9 | };
10 |
11 | export default SiteHeader;
12 |
--------------------------------------------------------------------------------
/src/__testfixtures__/HooksInlineExport.input.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 |
3 | export default function SiteFooter() {
4 | const [text] = useState("Something something");
5 | return (
6 | <>
7 | {text}
8 | My simple text
9 | >
10 | );
11 | }
12 |
--------------------------------------------------------------------------------
/src/__testfixtures__/HooksInlineExport.output.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 |
3 | import { useTranslation } from 'react-i18next';
4 |
5 | export default function SiteFooter() {
6 | const { t } = useTranslation();
7 | const [text] = useState(t('something_something'));
8 | return <>
9 | {text}
10 | {t('my_simple_text')}
11 | >;
12 | }
--------------------------------------------------------------------------------
/src/__testfixtures__/Imported.input.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { withTranslation } from 'react-i18next';
3 |
4 | const Simple = () => (
5 | My simple text
6 | );
7 |
--------------------------------------------------------------------------------
/src/__testfixtures__/Imported.output.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { withTranslation } from 'react-i18next';
3 |
4 | const Simple = () => (
5 | {t('my_simple_text')}
6 | );
7 |
--------------------------------------------------------------------------------
/src/__testfixtures__/NoChange.input.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { MuiPickersUtilsProvider } from 'material-ui-pickers';
3 | import { MuiThemeProvider } from '@material-ui/core/styles';
4 | import { BrowserRouter } from 'react-router-dom';
5 | import DateFnsUtils from '@date-io/date-fns';
6 | import muiTheme from './muiTheme';
7 | import Routes from './Routes';
8 |
9 |
10 | function App() {
11 | return (
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | );
20 | }
21 |
22 | export default App;
23 |
--------------------------------------------------------------------------------
/src/__testfixtures__/NoChange.output.tsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sibelius/ast-i18n/f5ac4c365dd5279d77685a7a966b574c95d7ac80/src/__testfixtures__/NoChange.output.tsx
--------------------------------------------------------------------------------
/src/__testfixtures__/Parameters.input.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 |
3 | const SiteHeader = () => {
4 | const [number] = useState(42);
5 | return
6 | My simple {number} text
7 | Other text
8 | Even more Text
9 | ;
10 | };
11 |
12 | export default SiteHeader;
13 |
--------------------------------------------------------------------------------
/src/__testfixtures__/Parameters.output.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 |
3 | import { useTranslation } from 'react-i18next';
4 |
5 | const SiteHeader = () => {
6 | const { t } = useTranslation();
7 | const [number] = useState(42);
8 | return (
9 | {t('my_simple_text', {
10 | arg0: number
11 | })}{t('other_text')}{t('even_more_text')}
12 | );
13 | };
14 |
15 | export default SiteHeader;
--------------------------------------------------------------------------------
/src/__testfixtures__/Parameters.resource.tsx:
--------------------------------------------------------------------------------
1 | export default {
2 | translation: {
3 | my_simple_text: `My simple {arg0} text`,
4 | even_more_text: `Even more Text`,
5 | other_text: `Other text`,
6 | },
7 | };
--------------------------------------------------------------------------------
/src/__testfixtures__/Props.input.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | type CustomProps = {
4 | title: string,
5 | }
6 | const Custom = (props: CustomProps) => {
7 | return (
8 |
9 | {props.title}
10 |
11 | )
12 | }
13 |
14 | const Component123 = () => (
15 |
16 | Simple text
17 |
18 |
19 | );
20 |
21 | export default Component123;
22 |
--------------------------------------------------------------------------------
/src/__testfixtures__/Props.output.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { useTranslation } from 'react-i18next';
4 |
5 | type CustomProps = {
6 | title: string,
7 | }
8 | const Custom = (props: CustomProps) => {
9 | return (
10 |
11 | {props.title}
12 |
13 | );
14 | }
15 |
16 | const Component123 = () => {
17 | const { t } = useTranslation();
18 |
19 | return (
20 |
21 | {t('simple_text')}
22 |
23 |
24 | );
25 | };
26 |
27 | export default Component123;
--------------------------------------------------------------------------------
/src/__testfixtures__/Props.resource.tsx:
--------------------------------------------------------------------------------
1 | export default {
2 | translation: {
3 | simple_text: `Simple text`,
4 | custom_name: `Custom name`,
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/src/__testfixtures__/Svg.input.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | const SomeIcon = () => {
4 | return (
5 |
17 | );
18 | };
19 |
20 | export default SomeIcon;
21 |
--------------------------------------------------------------------------------
/src/__testfixtures__/Svg.output.tsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sibelius/ast-i18n/f5ac4c365dd5279d77685a7a966b574c95d7ac80/src/__testfixtures__/Svg.output.tsx
--------------------------------------------------------------------------------
/src/__testfixtures__/Svg.resource.tsx:
--------------------------------------------------------------------------------
1 | export default {
2 | translation: {},
3 | };
4 |
--------------------------------------------------------------------------------
/src/__testfixtures__/Tsx.input.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | type CustomProps = {
4 | title: string,
5 | }
6 | const Custom = (props: CustomProps) => {
7 | return (
8 |
9 | {props.title}
10 |
11 | )
12 | }
13 |
14 | const Simple = () => (
15 |
16 | Simple text
17 |
18 |
19 | );
20 |
21 | export default Simple;
22 |
--------------------------------------------------------------------------------
/src/__testfixtures__/Tsx.output.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | import { useTranslation } from 'react-i18next';
4 |
5 | type CustomProps = {
6 | title: string,
7 | }
8 | const Custom = (props: CustomProps) => {
9 | return (
10 |
11 | {props.title}
12 |
13 | );
14 | }
15 |
16 | const Simple = () => {
17 | const { t } = useTranslation();
18 |
19 | return (
20 |
21 | {t('simple_text')}
22 |
23 |
24 | );
25 | };
26 |
27 | export default Simple;
--------------------------------------------------------------------------------
/src/__testfixtures__/Tsx.resource.tsx:
--------------------------------------------------------------------------------
1 | export default {
2 | translation: {
3 | simple_text: `Simple text`,
4 | custom_name: `Custom name`,
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/src/__testfixtures__/WithHoc.input.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { withSnackbar } from 'snackbar';
3 |
4 | function Simple() {
5 | return My simple text;
6 | }
7 |
8 | export default withSnackbar(Simple);
9 |
--------------------------------------------------------------------------------
/src/__testfixtures__/WithHoc.output.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { withSnackbar } from 'snackbar';
3 |
4 | import { useTranslation } from 'react-i18next';
5 |
6 | function Simple() {
7 | const { t } = useTranslation();
8 | return {t('my_simple_text')};
9 | }
10 |
11 | export default withSnackbar(Simple);
--------------------------------------------------------------------------------
/src/__testfixtures__/Yup.input.tsx:
--------------------------------------------------------------------------------
1 | import { withFormik } from 'formik';
2 | import * as yup from 'yup';
3 |
4 | const UserInnerForm = () => (
5 | user form here
6 | );
7 |
8 | type Values = {
9 | name: string,
10 | email: string,
11 | }
12 | const UserForm = withFormik({
13 | validationSchema: yup.object().shape({
14 | name: yup.string().required('Name is required'),
15 | email: yup.string().required('Email is required'),
16 | }),
17 | handleSubmit: (values: Values, formikBag) => {
18 | const { props } = formikBag;
19 | const { showSnackbar } = props;
20 |
21 | showSnackbar({ message: 'User editted successfully!'});
22 | },
23 | })(UserInnerForm);
24 |
25 | export default UserForm;
26 |
--------------------------------------------------------------------------------
/src/__testfixtures__/Yup.output.tsx:
--------------------------------------------------------------------------------
1 | import { withFormik } from 'formik';
2 | import * as yup from 'yup';
3 |
4 | import { withTranslation } from 'react-i18next';
5 |
6 | const UserInnerForm = () => (
7 | {t('user_form_here')}
8 | );
9 |
10 | type Values = {
11 | name: string,
12 | email: string,
13 | }
14 | const UserForm = withFormik({
15 | validationSchema: yup.object().shape({
16 | name: yup.string().required(t('name_is_required')),
17 | email: yup.string().required(t('email_is_required')),
18 | }),
19 | handleSubmit: (values: Values, formikBag) => {
20 | const { props } = formikBag;
21 | const { showSnackbar } = props;
22 |
23 | showSnackbar({ message: t('user_editted_successfully')});
24 | },
25 | })(UserInnerForm);
26 |
27 | export default withTranslation()(UserForm);
28 |
--------------------------------------------------------------------------------
/src/__testfixtures__/Yup.resource.tsx:
--------------------------------------------------------------------------------
1 | export default {
2 | translation: {
3 | user_form_here: `user form here`,
4 | name_is_required: `Name is required`,
5 | email_is_required: `Email is required`,
6 | user_editted_successfully: `User editted successfully!`,
7 | },
8 | };
9 |
--------------------------------------------------------------------------------
/src/cli.ts:
--------------------------------------------------------------------------------
1 | import yargs from 'yargs';
2 | import shell from 'shelljs';
3 |
4 | import { generateResources } from './generateResources';
5 | import { filterFiles, DEFAULT_TEST_FILE_REGEX } from './filterFiles';
6 |
7 | type Argv = {
8 | src: string,
9 | keyMaxLength: number,
10 | ignoreFilesRegex: string;
11 | }
12 |
13 | export const run = (argv: Argv) => {
14 | argv = yargs(argv || process.argv.slice(2))
15 | .usage(
16 | 'Extract all string inside JSXElement'
17 | )
18 | .default('src', process.cwd())
19 | .describe(
20 | 'src',
21 | 'The source to collect strings'
22 | )
23 | .default('keyMaxLength', 40)
24 | .describe(
25 | 'src',
26 | 'The source to collect strings'
27 | )
28 | .default('ignoreFilesRegex', DEFAULT_TEST_FILE_REGEX)
29 | .describe(
30 | 'ignoreFilesRegex',
31 | `The regex to ignore files in the source.\nThe files with this match is ignored by default:\n${DEFAULT_TEST_FILE_REGEX}`
32 | )
33 | .argv;
34 |
35 | const jsFiles = filterFiles(shell.find)(argv.src, argv.ignoreFilesRegex);
36 |
37 | generateResources(jsFiles, argv.keyMaxLength);
38 | };
39 |
--------------------------------------------------------------------------------
/src/config.ts:
--------------------------------------------------------------------------------
1 | import cosmiconfig from 'cosmiconfig';
2 |
3 | const explorer = cosmiconfig('ast');
4 |
5 | type ASTConfig = {
6 | blackListJsxAttributeName: string[],
7 | blackListCallExpressionCalle: string[],
8 | }
9 |
10 | const defaultConfig: ASTConfig = {
11 | blackListJsxAttributeName: [
12 | 'type',
13 | 'id',
14 | 'name',
15 | 'children',
16 | 'labelKey',
17 | 'valueKey',
18 | 'labelValue',
19 | 'className',
20 | ],
21 | blackListCallExpressionCalle: [
22 | 't',
23 | '_interopRequireDefault',
24 | 'require',
25 | 'routeTo',
26 | 'format',
27 | 'importScripts',
28 | ],
29 | };
30 |
31 | let config: ASTConfig | null = null;
32 |
33 | export const getAstConfig = (): ASTConfig => {
34 | if (config) {
35 | return config;
36 | }
37 |
38 | const result = explorer.searchSync();
39 |
40 | if (result) {
41 | config = {
42 | ...defaultConfig,
43 | ...result.config,
44 | };
45 |
46 | return config;
47 | }
48 |
49 | config = defaultConfig;
50 |
51 | return config;
52 | };
53 |
--------------------------------------------------------------------------------
/src/filterFiles.ts:
--------------------------------------------------------------------------------
1 | export const DEFAULT_TEST_FILE_REGEX =
2 | "(/__tests__/.*|(\\.|/)(test|spec))\\.(js|ts|tsx|jsx)?$";
3 |
4 | type ShellFind = (...path: Array) => string[];
5 |
6 | export const filterFiles = (shellFind: ShellFind) => (
7 | path: string,
8 | ignoreFilesRegex?: string
9 | ) => {
10 | const regex = new RegExp(ignoreFilesRegex || DEFAULT_TEST_FILE_REGEX);
11 | return shellFind(path).filter(
12 | (path) => /\.(js|ts|tsx)$/.test(path) && !regex.test(path)
13 | );
14 | };
15 |
--------------------------------------------------------------------------------
/src/generateResources.ts:
--------------------------------------------------------------------------------
1 | import fs from 'graceful-fs';
2 | import * as babel from '@babel/core';
3 | import prettier, { Options } from 'prettier';
4 |
5 | import BabelPluginI18n from './BabelPluginI18n';
6 |
7 | import babelConfig from '../babel.config.js';
8 |
9 | export const resource = (i18nResource: {[key: string]: string}) => {
10 | const formatted = Object.keys(i18nResource)
11 | .map(key => ` '${key}': \`${i18nResource[key]}\``)
12 | .join(',\n');
13 |
14 | return `export default {
15 | translation: {
16 | ${formatted}
17 | }
18 | }
19 | `
20 | };
21 |
22 | const prettierDefaultConfig: Options = {
23 | singleQuote: true,
24 | jsxSingleQuote: true,
25 | trailingComma: 'all',
26 | printWidth: 120,
27 | parser: 'babel',
28 | };
29 |
30 | export const getResourceSource = (i18nResource: {[key: string]: string}) => {
31 | const source = resource(i18nResource);
32 |
33 | return prettier.format(source, prettierDefaultConfig);
34 | };
35 |
36 | export const generateResources = (files: string[], keyMaxLength: number = 40) => {
37 | BabelPluginI18n.setMaxKeyLength(keyMaxLength);
38 |
39 | let phrases = [];
40 | for (const filename of files) {
41 | const source = fs.readFileSync(filename, 'utf8');
42 |
43 | try {
44 | babel.transformSync(source, {
45 | ast: false,
46 | code: true,
47 | plugins: [...babelConfig.plugins, BabelPluginI18n],
48 | sourceType: 'unambiguous',
49 | filename,
50 | });
51 |
52 | const newPhrases = BabelPluginI18n.getExtractedStrings();
53 |
54 | phrases = [
55 | ...phrases,
56 | ...newPhrases,
57 | ];
58 | } catch (err) {
59 | console.log('err: ', filename, err);
60 | }
61 | }
62 |
63 | const i18nMap = BabelPluginI18n.getI18Map();
64 |
65 | fs.writeFileSync('resource.tsx', resource(i18nMap));
66 |
67 | // tslint:disable-next-line
68 | console.log('generate resource file: resource.tsx');
69 |
70 | return i18nMap;
71 | };
72 |
--------------------------------------------------------------------------------
/src/i18nTransformerCodemod.ts:
--------------------------------------------------------------------------------
1 | import { API, FileInfo, Options, JSCodeshift, Collection } from 'jscodeshift';
2 | import { getStableKey } from './stableString';
3 | import { hasStringLiteralArguments, hasStringLiteralJSXAttribute, isSvgElement } from "./visitorChecks";
4 | import { CallExpression, ImportDeclaration, JSXAttribute, JSXText } from "@babel/types";
5 | import {
6 | ConditionalExpression,
7 | JSXElement,
8 | JSXExpressionContainer
9 | } from "ast-types/gen/nodes";
10 | import { NodePath } from "ast-types";
11 |
12 | const tCallExpression = (j: JSCodeshift, key: string) => {
13 | return j.callExpression(
14 | j.identifier('t'),
15 | [j.stringLiteral(key)],
16 | );
17 | };
18 |
19 | const getImportStatement = (useHoc: boolean = true, useHooks: boolean = false) => {
20 | if (useHoc && !useHooks) {
21 | return `import { withTranslation } from 'react-i18next';`;
22 | }
23 |
24 | if (useHooks && !useHoc) {
25 | return `import { useTranslation } from 'react-i18next';`;
26 | }
27 |
28 | return `import { useTranslation, withTranslation } from 'react-i18next';`;
29 | };
30 |
31 | const addI18nImport = (j: JSCodeshift, root: Collection, {useHooks, useHoc}: any) => {
32 | // TODO - handle hoc or hooks based on file
33 | const statement = getImportStatement(useHoc, useHooks);
34 |
35 | // check if there is a react-i18next import already
36 | const reactI18nNextImports = root
37 | .find(j.ImportDeclaration)
38 | .filter((path : NodePath) => path.node.source.value === 'react-i18next');
39 |
40 | if (reactI18nNextImports.length > 0) {
41 | return;
42 | }
43 |
44 | const imports = root.find(j.ImportDeclaration);
45 |
46 | if(imports.length > 0){
47 | j(imports.at(imports.length-1).get()).insertAfter(statement); // after the imports
48 | }else{
49 | root.get().node.program.body.unshift(statement); // begining of file
50 | }
51 | };
52 |
53 | function transform(file: FileInfo, api: API, options: Options) {
54 | const j = api.jscodeshift; // alias the jscodeshift API
55 | if (file.path.endsWith('.spec.js') || file.path.endsWith('.test.js')) {
56 | return;
57 | }
58 | const root = j(file.source); // parse JS code into an AST
59 |
60 | const printOptions = options.printOptions || {
61 | quote: 'single',
62 | trailingComma: false,
63 | // TODO make this configurable
64 | lineTerminator: '\n'
65 | };
66 |
67 | let hasI18nUsage = false;
68 |
69 | hasI18nUsage = translateJsxContent(j, root) || hasI18nUsage;
70 | hasI18nUsage = translateJsxProps(j, root) || hasI18nUsage;
71 | hasI18nUsage = translateFunctionArguments(j, root) || hasI18nUsage;
72 |
73 | if (hasI18nUsage) {
74 | // export default withTranslation()(Component)
75 | let hooksUsed = false;
76 | let hocUsed = false;
77 | root
78 | .find(j.ExportDefaultDeclaration)
79 | .filter(path => {
80 | let exportDeclaration = path.node.declaration;
81 | return j.Identifier.check(exportDeclaration)
82 | || j.CallExpression.check(exportDeclaration)
83 | || j.FunctionDeclaration.check(exportDeclaration);
84 | })
85 | .forEach(path => {
86 | let exportDeclaration = path.node.declaration;
87 |
88 | if (j.Identifier.check(exportDeclaration)) {
89 | const exportedName = exportDeclaration.name;
90 | const functions = findFunctionByIdentifier(j, exportedName, root);
91 | let hookFound = addUseHookToFunctionBody(
92 | j, functions
93 | );
94 |
95 | if(!hookFound) {
96 | hocUsed = true;
97 | path.node.declaration = withTranslationHoc(j, j.identifier(exportDeclaration.name));
98 | const classDeclaration = root.find(j.ClassDeclaration, (n) => n.id.name === exportDeclaration.name)
99 | .nodes()[0];
100 | if (classDeclaration) {
101 | const renderMethod = classDeclaration.body.body.find(
102 | n => j.ClassMethod.check(n) && n.key.name === 'render'
103 | );
104 | if (renderMethod) {
105 | renderMethod.body = j.blockStatement([
106 | createTranslationDefinition(j),
107 | ...renderMethod.body.body
108 | ])
109 | }
110 | }
111 |
112 | } else {
113 | hooksUsed = true;
114 | }
115 | return;
116 | }
117 | else if (j.CallExpression.check(exportDeclaration)) {
118 | if (exportDeclaration.callee.name === 'withTranslate') {
119 | return;
120 | }
121 |
122 | exportDeclaration.arguments.forEach(identifier => {
123 | const functions = findFunctionByIdentifier(j, identifier.name, root);
124 | hooksUsed = addUseHookToFunctionBody(j, functions) || hooksUsed;
125 | });
126 |
127 | if (!hooksUsed) {
128 | hooksUsed = true;
129 | path.node.declaration = withTranslationHoc(j, exportDeclaration);
130 | }
131 | } else if (j.FunctionDeclaration.check(exportDeclaration)) {
132 | hooksUsed = true;
133 | exportDeclaration.body = j.blockStatement([createUseTranslationCall(j), ...exportDeclaration.body.body])
134 | }
135 | });
136 |
137 | addI18nImport(j, root, {useHooks: hooksUsed, useHoc: hocUsed});
138 | // print
139 | return root.toSource(printOptions);
140 | }
141 | }
142 |
143 | function createUseTranslationCall(j: JSCodeshift) {
144 | return j.variableDeclaration('const',
145 | [j.variableDeclarator(
146 | j.identifier('{ t }'),
147 | j.callExpression(j.identifier('useTranslation'), [])
148 | )]
149 | );
150 | }
151 |
152 | function createTranslationDefinition(j: JSCodeshift) {
153 | return j.variableDeclaration('const',
154 | [j.variableDeclarator(
155 | j.identifier('{ t }'),
156 | j.memberExpression(j.thisExpression(), j.identifier('props'))
157 | )]
158 | );
159 | }
160 |
161 | function findFunctionByIdentifier(j: JSCodeshift, identifier: string, root: Collection) {
162 | return root.find(j.Function)
163 | .filter((p: NodePath) => {
164 | if (j.FunctionDeclaration.check(p.node)) {
165 | return p.node.id.name === identifier;
166 | }
167 | return p.parent.value.id && p.parent.value.id.name === identifier;
168 | });
169 | }
170 |
171 | function addUseHookToFunctionBody(j: JSCodeshift, functions: Collection) {
172 | let hookFound = false;
173 | functions
174 | .every(n => {
175 | hookFound = true;
176 | const body = n.node.body;
177 | n.node.body = j.BlockStatement.check(body)
178 | ? j.blockStatement([createUseTranslationCall(j), ...body.body])
179 | : j.blockStatement([createUseTranslationCall(j), j.returnStatement(body)])
180 | });
181 | return hookFound;
182 | }
183 |
184 | // Yup.string().required('this field is required')
185 | // showSnackbar({ message: 'ok' })
186 | function translateFunctionArguments(j: JSCodeshift, root: Collection) {
187 | let hasI18nUsage = false;
188 | root
189 | .find(j.CallExpression)
190 | .filter((path: NodePath) => !['classNames'].includes(path.value.callee.name))
191 | .filter((path: NodePath) => hasStringLiteralArguments(path))
192 | .forEach((path: NodePath) => {
193 | if (hasStringLiteralArguments(path)) {
194 | path.node.arguments = path.node.arguments.map(arg => {
195 | if (arg.type === 'StringLiteral' && arg.value) {
196 | const key = getStableKey(arg.value);
197 | hasI18nUsage = true;
198 |
199 | return tCallExpression(j, key)
200 | }
201 |
202 | if (arg.type === 'ObjectExpression') {
203 | arg.properties = arg.properties.map(prop => {
204 | if (prop.value && prop.value.type === 'StringLiteral') {
205 |
206 | const key = getStableKey(prop.value.value);
207 | prop.value = tCallExpression(j, key);
208 | hasI18nUsage = true;
209 | }
210 | return prop;
211 | });
212 | }
213 |
214 | return arg;
215 | })
216 | }
217 | });
218 |
219 | return hasI18nUsage;
220 | }
221 |
222 | //test
223 | function translateJsxContent(j: JSCodeshift, root: Collection) {
224 | let hasI18nUsage = false;
225 | root.find(j.JSXElement)
226 | .forEach((n: NodePath) => {
227 | const jsxContentNodes = n.value.children;
228 | let text = '';
229 | let translateArgs = [];
230 | let newChildren = [];
231 | for(let i = 0; i < jsxContentNodes.length; i++) {
232 | const element = jsxContentNodes[i];
233 | if (j.JSXText.check(element)) {
234 | if (element.value.trim().length > 0) {
235 | text += element.value;
236 | } else {
237 | newChildren.push(element);
238 | }
239 | continue;
240 | } else if (j.JSXExpressionContainer.check(element)) {
241 | translateArgs.push(element.expression);
242 | continue;
243 | }
244 | if (text.trim().length > 0) {
245 | hasI18nUsage = true;
246 | newChildren.push(buildTranslationWithArgumentsCall(
247 | j, translateArgs, text.trim()
248 | ));
249 | }
250 | text = '';
251 | translateArgs = [];
252 | newChildren.push(element);
253 | }
254 |
255 | if (text.trim().length > 0) {
256 | hasI18nUsage = true;
257 | newChildren.push(buildTranslationWithArgumentsCall(
258 | j, translateArgs, text.trim()
259 | ));
260 | }
261 | if (newChildren.length > 0) {
262 | //n.value.children = newChildren;
263 | n.replace(
264 | j.jsxElement(
265 | n.node.openingElement, n.node.closingElement,
266 | newChildren
267 | )
268 | )
269 | }
270 | });
271 |
272 | root
273 | .find(j.JSXText)
274 | .filter((path: NodePath) => path.node.value && path.node.value.trim())
275 | .replaceWith((path: NodePath) => {
276 | hasI18nUsage = true;
277 | const key = getStableKey(path.node.value);
278 | return j.jsxExpressionContainer(j.callExpression(j.identifier('t'), [j.literal(key)]))
279 | });
280 | return hasI18nUsage;
281 | }
282 |
283 | function translateJsxProps(j: JSCodeshift, root: Collection) {
284 | let hasI18nUsage = false;
285 | //
286 | root
287 | .find(j.JSXElement)
288 | .filter((path: NodePath) => !isSvgElement(path))
289 | .find(j.JSXAttribute)
290 | .filter((path: NodePath) => hasStringLiteralJSXAttribute(path))
291 | .forEach((path: NodePath) => {
292 | if (!path.node.value || !path.node.value.value) {
293 | return;
294 | }
295 | const key = getStableKey(path.node.value.value);
296 | hasI18nUsage = true;
297 |
298 | path.node.value = j.jsxExpressionContainer(
299 | tCallExpression(j, key),
300 | );
301 | });
302 |
303 | //
304 | root
305 | .find(j.JSXExpressionContainer)
306 | .filter((path: NodePath) => {
307 | return path.node.expression && j.StringLiteral.check(path.node.expression)
308 | })
309 | .forEach(path => {
310 | const key = getStableKey(path.node.expression.value);
311 | hasI18nUsage = true;
312 |
313 | path.node.expression = tCallExpression(j, key);
314 | });
315 |
316 | //
317 | root
318 | .find(j.JSXExpressionContainer)
319 | .filter((path: NodePath) => {
320 | return path.node.expression && j.ConditionalExpression.check(path.node.expression)
321 | })
322 | .forEach(((path: NodePath) => {
323 | let expression = path.value.expression;
324 | if (j.Literal.check(expression.consequent)) {
325 | hasI18nUsage = true;
326 | const key = getStableKey(expression.consequent.value);
327 | expression.consequent = tCallExpression(j, key);
328 | }
329 | if (j.Literal.check(expression.alternate)) {
330 | hasI18nUsage = true;
331 | const key = getStableKey(expression.alternate.value);
332 | expression.alternate = tCallExpression(j, key);
333 | }
334 | hasI18nUsage = true;
335 | }));
336 |
337 | return hasI18nUsage;
338 | }
339 |
340 | function buildTranslationWithArgumentsCall(j: JSCodeshift, translateArgs: any, text: string) {
341 | const translationCallArguments = [
342 | j.literal(getStableKey(text)),
343 | ] as any;
344 | if (translateArgs.length > 0) {
345 | translationCallArguments.push(
346 | j.objectExpression(
347 | translateArgs.map((expression: any, index: any) =>
348 | j.property('init', j.identifier('arg' + index), expression )
349 | )
350 | )
351 | )
352 | }
353 | return j.jsxExpressionContainer(
354 | j.callExpression(j.identifier('t'), translationCallArguments));
355 | }
356 |
357 | function withTranslationHoc(j: JSCodeshift, identifier: any) {
358 | return j.callExpression(
359 | j.callExpression(
360 | j.identifier('withTranslation'),
361 | [],
362 | ),
363 | [
364 | identifier
365 | ],
366 | )
367 | }
368 |
369 |
370 | module.exports = transform;
371 | module.exports.parser = 'tsx';
372 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | /*
2 | get list of files from command line
3 | parse each file to find JSXText (where the translable string should be extracted from)
4 | generate a stable key for each string
5 | generate i18n files based on this
6 | */
7 | import shell from 'shelljs';
8 | import yargs from 'yargs';
9 |
10 | import { generateResources } from './generateResources';
11 |
12 | const argv = yargs
13 | .usage(
14 | 'Extract all string inside JSXElement'
15 | )
16 | .default('src', process.cwd())
17 | .describe(
18 | 'src',
19 | 'The source to collect strings'
20 | )
21 | .default('keyMaxLength', 40)
22 | .describe(
23 | 'src',
24 | 'The source to collect strings'
25 | )
26 | .argv;
27 |
28 | const jsFiles = shell.find(argv.src).filter(path => /\.(js|ts|tsx)$/.test(path));
29 | generateResources(jsFiles, argv.keyMaxLength);
30 |
--------------------------------------------------------------------------------
/src/stableString.ts:
--------------------------------------------------------------------------------
1 | import { remove } from 'diacritics';
2 | import slugify from 'slugify';
3 |
4 | export const getStableKey = (str: string, keyMaxLength: number = 40) => {
5 | const cleanStr = remove(str)
6 | .toLocaleLowerCase()
7 | .normalize('NFD')
8 | .replace(/[\u0300-\u036f]/g, "")
9 | .trim()
10 | .replace(/ +/g, '_')
11 | .replace(/\s+/g, '')
12 | .replace(/[.*+?^${}()|[\]\\\/-:,!"]/g, '')
13 | .replace(/'+/g, '')
14 | .replace(/[^\x00-\x7F]/g, "")
15 | .slice(0, keyMaxLength);
16 |
17 | return slugify(cleanStr);
18 | };
19 |
20 | export const getStableValue = (str: string) => {
21 | return str
22 | .trim()
23 | .replace(/\s+/g, ' ')
24 | };
25 |
--------------------------------------------------------------------------------
/src/visitorChecks.ts:
--------------------------------------------------------------------------------
1 | import { CallExpression, JSXAttribute } from '@babel/types';
2 | import { NodePath } from "ast-types";
3 | import { JSXElement, JSXIdentifier } from "ast-types/gen/nodes";
4 | import { getAstConfig } from './config';
5 |
6 | const svgElementNames = ["svg", 'path', 'g'];
7 |
8 | export const hasStringLiteralJSXAttribute = (path: NodePath) => {
9 | if (!path.node.value) {
10 | return false;
11 | }
12 |
13 | if (path.node.value.type !== 'StringLiteral') {
14 | return false;
15 | }
16 |
17 | const { blackListJsxAttributeName } = getAstConfig();
18 |
19 | if (blackListJsxAttributeName.indexOf(path.node.name.name) > -1) {
20 | return false;
21 | }
22 |
23 | return true;
24 | };
25 |
26 | export const hasStringLiteralArguments = (path: NodePath) => {
27 | const { callee } = path.node;
28 |
29 | const { blackListCallExpressionCalle } = getAstConfig();
30 |
31 | if (callee.type === 'Identifier') {
32 | const { callee } = path.node;
33 |
34 | if (blackListCallExpressionCalle.indexOf(callee.name) > -1) {
35 | return false;
36 | }
37 | }
38 |
39 | if (callee.type === 'Import') {
40 | return false;
41 | }
42 |
43 | if (callee.type === 'MemberExpression') {
44 | const { property } = path.node.callee;
45 |
46 | if (property && property.type === 'Identifier' && property.name === 'required') {
47 | if (path.node.arguments.length === 1) {
48 | if (path.node.arguments[0].type === 'StringLiteral') {
49 | return true;
50 | }
51 | }
52 |
53 | return true;
54 | }
55 |
56 | // do not convert react expressions
57 | return false;
58 | }
59 |
60 | if (path.node.arguments.length === 0) {
61 | return false;
62 | }
63 |
64 | for (const arg of path.node.arguments) {
65 | // myFunc('ok')
66 | if (arg.type === 'StringLiteral') {
67 | return true;
68 | }
69 |
70 | // showSnackbar({ message: 'ok' });
71 | if (arg.type === 'ObjectExpression') {
72 | if (arg.properties.length === 0) {
73 | continue;
74 | }
75 |
76 | for (const prop of arg.properties) {
77 | if (prop.value && prop.value.type === 'StringLiteral') {
78 | return true;
79 | }
80 | }
81 | }
82 |
83 | // myFunc(['ok', 'blah']) - should we handle this case?
84 | }
85 |
86 | return false;
87 | };
88 |
89 | export const isSvgElement = (path: NodePath) => {
90 | const jsxIdentifier = path.node.openingElement.name = path.node.openingElement.name as JSXIdentifier;
91 | return svgElementNames.includes(jsxIdentifier.name);
92 | };
93 |
94 | export const isSvgElementAttribute = (path: NodePath) => {
95 | if (!path.parent || !path.parent.name) {
96 | return false;
97 | }
98 | return svgElementNames.includes(path.parent.name.name);
99 | };
100 |
--------------------------------------------------------------------------------
/test/testUtils.ts:
--------------------------------------------------------------------------------
1 | // based on jscodeshift code base
2 | import fs from 'fs';
3 | import path from 'path';
4 |
5 | function runInlineTest(module, options, input, expectedOutput) {
6 | // Handle ES6 modules using default export for the transform
7 | const transform = module.default ? module.default : module;
8 |
9 | // Jest resets the module registry after each test, so we need to always get
10 | // a fresh copy of jscodeshift on every test run.
11 | let jscodeshift = require('jscodeshift');
12 | if (module.parser) {
13 | jscodeshift = jscodeshift.withParser(module.parser);
14 | }
15 |
16 | const output = transform(
17 | input,
18 | {
19 | jscodeshift,
20 | stats: () => {},
21 | },
22 | options || {}
23 | );
24 | expect((output || '').trim()).toEqual(expectedOutput.trim());
25 | }
26 | exports.runInlineTest = runInlineTest;
27 |
28 | /**
29 | * Utility function to run a jscodeshift script within a unit test. This makes
30 | * several assumptions about the environment:
31 | *
32 | * - `dirName` contains the name of the directory the test is located in. This
33 | * should normally be passed via __dirname.
34 | * - The test should be located in a subdirectory next to the transform itself.
35 | * Commonly tests are located in a directory called __tests__.
36 | * - `transformName` contains the filename of the transform being tested,
37 | * excluding the .js extension.
38 | * - `testFilePrefix` optionally contains the name of the file with the test
39 | * data. If not specified, it defaults to the same value as `transformName`.
40 | * This will be suffixed with ".input.js" for the input file and ".output.js"
41 | * for the expected output. For example, if set to "foo", we will read the
42 | * "foo.input.js" file, pass this to the transform, and expect its output to
43 | * be equal to the contents of "foo.output.js".
44 | * - Test data should be located in a directory called __testfixtures__
45 | * alongside the transform and __tests__ directory.
46 | */
47 | function runTest(dirName, transformName, options, testFilePrefix) {
48 | if (!testFilePrefix) {
49 | testFilePrefix = transformName;
50 | }
51 |
52 | const fixtureDir = path.join(dirName, '..', '__testfixtures__');
53 | const inputPath = path.join(fixtureDir, testFilePrefix + '.input.tsx');
54 | const source = fs.readFileSync(inputPath, 'utf8');
55 | const expectedOutput = fs.readFileSync(
56 | path.join(fixtureDir, testFilePrefix + '.output.tsx'),
57 | 'utf8'
58 | );
59 | // Assumes transform is one level up from __tests__ directory
60 | const module = require(path.join(dirName, '..', transformName + '.ts'));
61 | runInlineTest(module, options, {
62 | path: inputPath,
63 | source
64 | }, expectedOutput);
65 | }
66 | exports.runTest = runTest;
67 |
68 | /**
69 | * Handles some boilerplate around defining a simple jest/Jasmine test for a
70 | * jscodeshift transform.
71 | */
72 | function defineTest(dirName, transformName, options, testFilePrefix, only: boolean = false) {
73 | const testName = testFilePrefix
74 | ? `transforms correctly using "${testFilePrefix}" data`
75 | : 'transforms correctly';
76 | describe(transformName, () => {
77 | const myIt = only ? it.only : it;
78 |
79 | myIt(testName, () => {
80 | runTest(dirName, transformName, options, testFilePrefix);
81 | });
82 | });
83 | }
84 | exports.defineTest = defineTest;
85 |
86 | function defineInlineTest(module, options, input, expectedOutput, testName) {
87 | it(testName || 'transforms correctly', () => {
88 | runInlineTest(module, options, {
89 | source: input
90 | }, expectedOutput);
91 | });
92 | }
93 | exports.defineInlineTest = defineInlineTest;
94 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Basic Options */
4 | "target": "es2017", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */
5 | "module": "umd", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */
6 | "lib": [ /* Specify library files to be included in the compilation. */
7 | "esnext",
8 | "dom"
9 | ],
10 | // "allowJs": true, /* Allow javascript files to be compiled. */
11 | // "checkJs": true, /* Report errors in .js files. */
12 | "jsx": "react", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
13 | // "declaration": true, /* Generates corresponding '.d.ts' file. */
14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
15 | // "sourceMap": true, /* Generates corresponding '.map' file. */
16 | // "outFile": "./", /* Concatenate and emit output to single file. */
17 | "outDir": "./distTs", /* Redirect output structure to the directory. */
18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
19 | // "composite": true, /* Enable project compilation */
20 | // "removeComments": true, /* Do not emit comments to output. */
21 | "noEmit": true, /* Do not emit outputs. */
22 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */
23 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
24 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
25 |
26 | /* Strict Type-Checking Options */
27 | "strict": true, /* Enable all strict type-checking options. */
28 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
29 | // "strictNullChecks": true, /* Enable strict null checks. */
30 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */
31 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
32 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
33 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
34 |
35 | /* Additional Checks */
36 | "noUnusedLocals": true, /* Report errors on unused locals. */
37 | "noUnusedParameters": true, /* Report errors on unused parameters. */
38 | "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
39 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
40 |
41 | /* Module Resolution Options */
42 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
43 | "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
44 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
45 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
46 | // "typeRoots": [], /* List of folders to include type definitions from. */
47 | // "types": [], /* Type declaration files to be included in compilation. */
48 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
49 | "resolveJsonModule": true,
50 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
51 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
52 |
53 | /* Source Map Options */
54 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
55 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
58 |
59 | /* Experimental Options */
60 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
62 | },
63 | "include": [
64 | "src/**/*",
65 | "src/__test__/**/*"
66 | ]
67 | }
68 |
--------------------------------------------------------------------------------