Skip to content

Commit

Permalink
feat: Add init command (#253)
Browse files Browse the repository at this point in the history
  • Loading branch information
tricoder42 authored Aug 8, 2018
1 parent 3ca052f commit afdc03c
Show file tree
Hide file tree
Showing 9 changed files with 310 additions and 11 deletions.
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,16 @@
# Change Log

<a name="2.4.0"></a>
## [2.4.0 (unreleased)](https://github.com/lingui/js-lingui/compare/v2.3.0...v2.4.0) (TBA)

Support for Create React App.

### New Features

* New `lingui init` command which detects project type and install all required packages
* `lingui extract` detects ``create-react-app` projects and extracts messages using
``rect-app`` babel preset

<a name="2.3.0"></a>
## [2.3.0](https://github.com/lingui/js-lingui/compare/v2.2.0...v2.3.0) (2018-07-23)

Expand Down
14 changes: 14 additions & 0 deletions docs/ref/cli.rst
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,20 @@ or locally:
Commands
========

``init``
--------

.. lingui-cli:: init [--dry-run]

Installs all required packages based on project type. Recognized projects are:

- `Create React App <https://github.com/facebook/create-react-app>`_
- General React app

.. lingui-cli-option:: --dry-run

Output commands which would run, but don't execute them.

``add-locale``
--------------

Expand Down
31 changes: 24 additions & 7 deletions docs/tutorials/react.rst
Original file line number Diff line number Diff line change
Expand Up @@ -67,24 +67,41 @@ We're trying to use most of them to show the full power of jsLingui.

Let's start with the three major packages:

``@lingui/cli``
CLI for i18n management and working with message catalogs

``@lingui/react``
React components for translation and formatting

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

``@lingui/cli``
CLI for working with message catalogs

1. Install ``@lingui/babel-preset-react`` as a development dependency,
``@lingui/react`` as a runtime dependency and ``@lingui/cli`` globally:
1. ``@lingui/cli`` comes with handy :cli:`init` command, which detects the
project type and install all required packages automatically. Feel free to run
it with ``lingui init --dry-run`` option to inspect what commands will be run:

.. code-block:: shell
npm install -g @lingui/cli
npm install --save @lingui/react
npm install --save-dev @lingui/babel-preset-react
lingui init
Yarn is supported as well:

.. code-block:: shell
yarn global add @lingui/cli
lingui init
.. note::

Under the hood it installs ``@lingui/babel-preset-react`` as a development
dependency and ``@lingui/react`` as a runtime dependency (in React projects):

.. code-block:: shell
npm install --save @lingui/react
npm install --save-dev @lingui/babel-preset-react
2. Add ``@lingui/babel-preset-react`` preset to Babel config (e.g: ``.babelrc``):

Expand Down
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
"cli-table": "^0.3.1",
"commander": "^2.16.0",
"glob": "^7.1.2",
"inquirer": "^6.1.0",
"make-plural": "^4.1.1",
"messageformat-parser": "^2.0.0",
"mkdirp": "^0.5.1",
Expand Down
78 changes: 78 additions & 0 deletions packages/cli/src/lingui-init.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// @flow
import path from "path"
import fs from "fs"
import program from "commander"
import { execSync } from "child_process"
import inquirer from "inquirer"
import chalk from "chalk"

import { projectType, detect } from "./api/detect"

function hasYarn() {
return fs.existsSync(path.resolve("yarn.lock"))
}

function makeInstall() {
const withYarn = hasYarn()

return (packageName, dev = false) =>
withYarn
? `yarn add ${dev ? "--dev " : ""}${packageName}`
: `npm install ${dev ? "--save-dev" : "--save"} ${packageName}`
}

export async function installPackages(dryRun: boolean) {
const install = makeInstall()

const type = detect()
const usesReact = type === projectType.CRA || type === projectType.REACT

const packages = [
...(usesReact
? [["@lingui/react"], ["@lingui/babel-preset-react", true]]
: [["@lingui/core"], ["@lingui/babel-preset-js", true]])
].filter(Boolean)

const verbosePackages = packages
.map(([packageName]) => chalk.yellow(packageName))
.join(", ")
const { confirm } = await inquirer.prompt({
type: "confirm",
name: "confirm",
message: `Do you want to install ${verbosePackages}?`
})

if (!confirm) return false

const commands = packages.map(([packageName, dev]) =>
install(packageName, dev)
)
if (dryRun) {
commands.forEach(command => console.log(command))
} else {
commands.forEach(command => execSync(command))
}

return true
}

type CommandInit = {|
dryRun: boolean
|}

export async function command(program: CommandInit) {
await installPackages(program.dryRun)

return true
}

if (require.main === module) {
program
.option(
"--dry-run",
"Output commands that would be run, but don't execute them."
)
.parse(process.argv)

command(program)
}
121 changes: 121 additions & 0 deletions packages/cli/src/lingui-init.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import mockFs from "mock-fs"
import { installPackages } from "./lingui-init"
import { mockConsole } from "./mocks"
import inquirer from "inquirer"

describe("lingui init", function() {
let inquirerPrompt

beforeAll(() => {
inquirerPrompt = inquirer.prompt
inquirer.prompt = jest.fn()
})

afterAll(() => {
inquirer.prompt = inquirerPrompt
})

afterEach(() => {
mockFs.restore()
inquirer.prompt.mockReset()
})

describe("install packages", function() {
beforeEach(() => {
inquirer.prompt = jest.fn(prompt =>
Promise.resolve({ [prompt.name]: true })
)
})

it("should use yarn when available", () => {
mockFs({
"yarn.lock": mockFs.file(),
"package.json": JSON.stringify({
dependencies: {}
})
})

expect.assertions(3)
return mockConsole(console =>
installPackages(true).then(result => {
expect(result).toBeTruthy()
expect(console.log).toBeCalledWith("yarn add @lingui/core")
expect(console.log).toBeCalledWith(
"yarn add --dev @lingui/babel-preset-js"
)
})
)
})

it("should use npm when yarn isn't available", () => {
mockFs({
"package.json": JSON.stringify({
dependencies: {}
})
})

expect.assertions(3)
return mockConsole(console =>
installPackages(true)
.then(result => {
expect(result).toBeTruthy()
expect(console.log).toBeCalledWith(
"npm install --save @lingui/core"
)
expect(console.log).toBeCalledWith(
"npm install --save-dev @lingui/babel-preset-js"
)
})
.catch(error => console.log({ error }))
)
})

it("should detect create-react-app project", () => {
mockFs({
"package.json": JSON.stringify({
dependencies: {
"react-scripts": "2.0.0"
}
})
})

expect.assertions(3)
return mockConsole(console =>
installPackages(true)
.then(result => {
expect(result).toBeTruthy()
expect(console.log).toBeCalledWith(
"npm install --save @lingui/react"
)
expect(console.log).toBeCalledWith(
"npm install --save-dev @lingui/babel-preset-react"
)
})
.catch(error => console.log({ error }))
)
})

it("should install core only for other projects", () => {
mockFs({
"package.json": JSON.stringify({
dependencies: {}
})
})

expect.assertions(3)
return mockConsole(console =>
installPackages(true)
.then(result => {
expect(result).toBeTruthy()
expect(console.log).toBeCalledWith(
"npm install --save @lingui/core"
)
expect(console.log).toBeCalledWith(
"npm install --save-dev @lingui/babel-preset-js"
)
})
.catch(error => console.log({ error }))
)
})
})
})
1 change: 1 addition & 0 deletions packages/cli/src/lingui.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ try {

program
.version(version)
.command("init", "Install all required packages")
.command(
"add-locale [locales...]",
"Add new locale (generate empty message catalogues for this locale)"
Expand Down
16 changes: 13 additions & 3 deletions packages/cli/src/mocks.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
export function mockConsole(testCase, mock = {}) {
function restoreConsole() {
global.console = originalConsole
}

const originalConsole = global.console

const defaults = {
Expand All @@ -11,12 +15,18 @@ export function mockConsole(testCase, mock = {}) {
...mock
}

let result
try {
testCase(global.console)
result = testCase(global.console)
} catch (e) {
global.console = originalConsole
restoreConsole()
throw e
}

global.console = originalConsole
if (result && typeof result.then === "function") {
return result.then(restoreConsole).catch(restoreConsole)
} else {
restoreConsole()
return result
}
}
Loading

0 comments on commit afdc03c

Please sign in to comment.