Skip to content

Commit 4899b5d

Browse files
tricoder42Photonios
authored andcommitted
feat: Add init command (lingui#253)
1 parent 76faf74 commit 4899b5d

9 files changed

Lines changed: 310 additions & 11 deletions

File tree

CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,16 @@
11
# Change Log
22

3+
<a name="2.4.0"></a>
4+
## [2.4.0 (unreleased)](https://github.com/lingui/js-lingui/compare/v2.3.0...v2.4.0) (TBA)
5+
6+
Support for Create React App.
7+
8+
### New Features
9+
10+
* New `lingui init` command which detects project type and install all required packages
11+
* `lingui extract` detects ``create-react-app` projects and extracts messages using
12+
``rect-app`` babel preset
13+
314
<a name="2.3.0"></a>
415
## [2.3.0](https://github.com/lingui/js-lingui/compare/v2.2.0...v2.3.0) (2018-07-23)
516

docs/ref/cli.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,20 @@ or locally:
6767
Commands
6868
========
6969

70+
``init``
71+
--------
72+
73+
.. lingui-cli:: init [--dry-run]
74+
75+
Installs all required packages based on project type. Recognized projects are:
76+
77+
- `Create React App <https://github.com/facebook/create-react-app>`_
78+
- General React app
79+
80+
.. lingui-cli-option:: --dry-run
81+
82+
Output commands which would run, but don't execute them.
83+
7084
``add-locale``
7185
--------------
7286

docs/tutorials/react.rst

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -67,24 +67,41 @@ We're trying to use most of them to show the full power of jsLingui.
6767

6868
Let's start with the three major packages:
6969

70+
``@lingui/cli``
71+
CLI for i18n management and working with message catalogs
72+
7073
``@lingui/react``
7174
React components for translation and formatting
7275

7376
``@lingui/babel-preset-react``
7477
Transforms messages wrapped in components from ``@lingui/react`` to ICU
7578
MessageFormat and validates them
7679

77-
``@lingui/cli``
78-
CLI for working with message catalogs
79-
80-
1. Install ``@lingui/babel-preset-react`` as a development dependency,
81-
``@lingui/react`` as a runtime dependency and ``@lingui/cli`` globally:
80+
1. ``@lingui/cli`` comes with handy :cli:`init` command, which detects the
81+
project type and install all required packages automatically. Feel free to run
82+
it with ``lingui init --dry-run`` option to inspect what commands will be run:
8283

8384
.. code-block:: shell
8485
8586
npm install -g @lingui/cli
86-
npm install --save @lingui/react
87-
npm install --save-dev @lingui/babel-preset-react
87+
lingui init
88+
89+
Yarn is supported as well:
90+
91+
.. code-block:: shell
92+
93+
yarn global add @lingui/cli
94+
lingui init
95+
96+
.. note::
97+
98+
Under the hood it installs ``@lingui/babel-preset-react`` as a development
99+
dependency and ``@lingui/react`` as a runtime dependency (in React projects):
100+
101+
.. code-block:: shell
102+
103+
npm install --save @lingui/react
104+
npm install --save-dev @lingui/babel-preset-react
88105
89106
2. Add ``@lingui/babel-preset-react`` preset to Babel config (e.g: ``.babelrc``):
90107

packages/cli/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
"cli-table": "^0.3.1",
5151
"commander": "^2.16.0",
5252
"glob": "^7.1.2",
53+
"inquirer": "^6.1.0",
5354
"make-plural": "^4.1.1",
5455
"messageformat-parser": "^2.0.0",
5556
"mkdirp": "^0.5.1",

packages/cli/src/lingui-init.js

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// @flow
2+
import path from "path"
3+
import fs from "fs"
4+
import program from "commander"
5+
import { execSync } from "child_process"
6+
import inquirer from "inquirer"
7+
import chalk from "chalk"
8+
9+
import { projectType, detect } from "./api/detect"
10+
11+
function hasYarn() {
12+
return fs.existsSync(path.resolve("yarn.lock"))
13+
}
14+
15+
function makeInstall() {
16+
const withYarn = hasYarn()
17+
18+
return (packageName, dev = false) =>
19+
withYarn
20+
? `yarn add ${dev ? "--dev " : ""}${packageName}`
21+
: `npm install ${dev ? "--save-dev" : "--save"} ${packageName}`
22+
}
23+
24+
export async function installPackages(dryRun: boolean) {
25+
const install = makeInstall()
26+
27+
const type = detect()
28+
const usesReact = type === projectType.CRA || type === projectType.REACT
29+
30+
const packages = [
31+
...(usesReact
32+
? [["@lingui/react"], ["@lingui/babel-preset-react", true]]
33+
: [["@lingui/core"], ["@lingui/babel-preset-js", true]])
34+
].filter(Boolean)
35+
36+
const verbosePackages = packages
37+
.map(([packageName]) => chalk.yellow(packageName))
38+
.join(", ")
39+
const { confirm } = await inquirer.prompt({
40+
type: "confirm",
41+
name: "confirm",
42+
message: `Do you want to install ${verbosePackages}?`
43+
})
44+
45+
if (!confirm) return false
46+
47+
const commands = packages.map(([packageName, dev]) =>
48+
install(packageName, dev)
49+
)
50+
if (dryRun) {
51+
commands.forEach(command => console.log(command))
52+
} else {
53+
commands.forEach(command => execSync(command))
54+
}
55+
56+
return true
57+
}
58+
59+
type CommandInit = {|
60+
dryRun: boolean
61+
|}
62+
63+
export async function command(program: CommandInit) {
64+
await installPackages(program.dryRun)
65+
66+
return true
67+
}
68+
69+
if (require.main === module) {
70+
program
71+
.option(
72+
"--dry-run",
73+
"Output commands that would be run, but don't execute them."
74+
)
75+
.parse(process.argv)
76+
77+
command(program)
78+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import mockFs from "mock-fs"
2+
import { installPackages } from "./lingui-init"
3+
import { mockConsole } from "./mocks"
4+
import inquirer from "inquirer"
5+
6+
describe("lingui init", function() {
7+
let inquirerPrompt
8+
9+
beforeAll(() => {
10+
inquirerPrompt = inquirer.prompt
11+
inquirer.prompt = jest.fn()
12+
})
13+
14+
afterAll(() => {
15+
inquirer.prompt = inquirerPrompt
16+
})
17+
18+
afterEach(() => {
19+
mockFs.restore()
20+
inquirer.prompt.mockReset()
21+
})
22+
23+
describe("install packages", function() {
24+
beforeEach(() => {
25+
inquirer.prompt = jest.fn(prompt =>
26+
Promise.resolve({ [prompt.name]: true })
27+
)
28+
})
29+
30+
it("should use yarn when available", () => {
31+
mockFs({
32+
"yarn.lock": mockFs.file(),
33+
"package.json": JSON.stringify({
34+
dependencies: {}
35+
})
36+
})
37+
38+
expect.assertions(3)
39+
return mockConsole(console =>
40+
installPackages(true).then(result => {
41+
expect(result).toBeTruthy()
42+
expect(console.log).toBeCalledWith("yarn add @lingui/core")
43+
expect(console.log).toBeCalledWith(
44+
"yarn add --dev @lingui/babel-preset-js"
45+
)
46+
})
47+
)
48+
})
49+
50+
it("should use npm when yarn isn't available", () => {
51+
mockFs({
52+
"package.json": JSON.stringify({
53+
dependencies: {}
54+
})
55+
})
56+
57+
expect.assertions(3)
58+
return mockConsole(console =>
59+
installPackages(true)
60+
.then(result => {
61+
expect(result).toBeTruthy()
62+
expect(console.log).toBeCalledWith(
63+
"npm install --save @lingui/core"
64+
)
65+
expect(console.log).toBeCalledWith(
66+
"npm install --save-dev @lingui/babel-preset-js"
67+
)
68+
})
69+
.catch(error => console.log({ error }))
70+
)
71+
})
72+
73+
it("should detect create-react-app project", () => {
74+
mockFs({
75+
"package.json": JSON.stringify({
76+
dependencies: {
77+
"react-scripts": "2.0.0"
78+
}
79+
})
80+
})
81+
82+
expect.assertions(3)
83+
return mockConsole(console =>
84+
installPackages(true)
85+
.then(result => {
86+
expect(result).toBeTruthy()
87+
expect(console.log).toBeCalledWith(
88+
"npm install --save @lingui/react"
89+
)
90+
expect(console.log).toBeCalledWith(
91+
"npm install --save-dev @lingui/babel-preset-react"
92+
)
93+
})
94+
.catch(error => console.log({ error }))
95+
)
96+
})
97+
98+
it("should install core only for other projects", () => {
99+
mockFs({
100+
"package.json": JSON.stringify({
101+
dependencies: {}
102+
})
103+
})
104+
105+
expect.assertions(3)
106+
return mockConsole(console =>
107+
installPackages(true)
108+
.then(result => {
109+
expect(result).toBeTruthy()
110+
expect(console.log).toBeCalledWith(
111+
"npm install --save @lingui/core"
112+
)
113+
expect(console.log).toBeCalledWith(
114+
"npm install --save-dev @lingui/babel-preset-js"
115+
)
116+
})
117+
.catch(error => console.log({ error }))
118+
)
119+
})
120+
})
121+
})

packages/cli/src/lingui.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ try {
1111

1212
program
1313
.version(version)
14+
.command("init", "Install all required packages")
1415
.command(
1516
"add-locale [locales...]",
1617
"Add new locale (generate empty message catalogues for this locale)"

packages/cli/src/mocks.js

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
export function mockConsole(testCase, mock = {}) {
2+
function restoreConsole() {
3+
global.console = originalConsole
4+
}
5+
26
const originalConsole = global.console
37

48
const defaults = {
@@ -11,12 +15,18 @@ export function mockConsole(testCase, mock = {}) {
1115
...mock
1216
}
1317

18+
let result
1419
try {
15-
testCase(global.console)
20+
result = testCase(global.console)
1621
} catch (e) {
17-
global.console = originalConsole
22+
restoreConsole()
1823
throw e
1924
}
2025

21-
global.console = originalConsole
26+
if (result && typeof result.then === "function") {
27+
return result.then(restoreConsole).catch(restoreConsole)
28+
} else {
29+
restoreConsole()
30+
return result
31+
}
2232
}

0 commit comments

Comments
 (0)