Skip to content

Commit 9d81b31

Browse files
authored
pull command feature (#33)
1 parent 63dd176 commit 9d81b31

File tree

6 files changed

+291
-29
lines changed

6 files changed

+291
-29
lines changed

src/commands/pull.ts

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import * as fs from 'fs'
2+
import * as path from 'path'
3+
import { promisify } from 'util'
4+
import { Arguments, Argv } from 'yargs'
5+
6+
import { debug as Debug } from 'debug'
7+
const debug = Debug('vue-i18n-locale-message:commands:pull')
8+
9+
const mkdirPromisify = promisify(fs.mkdir)
10+
const writeFilePromisify = promisify(fs.writeFile)
11+
12+
import { resolve, loadProvider, loadProviderConf, DEFUALT_CONF } from '../utils'
13+
import { ProviderPullResource, Locale, LocaleMessage } from '../../types'
14+
15+
type PullOptions = {
16+
provider: string
17+
conf?: string
18+
output: string
19+
locales?: string
20+
dryRun?: boolean
21+
}
22+
23+
export const command = 'pull'
24+
export const aliases = 'pl'
25+
export const describe = 'pull locale messages from localization service'
26+
27+
export const builder = (args: Argv): Argv<PullOptions> => {
28+
return args
29+
.option('provider', {
30+
type: 'string',
31+
alias: 'p',
32+
describe: 'the target localization service provider',
33+
demandOption: true
34+
})
35+
.option('conf', {
36+
type: 'string',
37+
alias: 'c',
38+
describe: 'the json file configration of localization service provider'
39+
})
40+
.option('output', {
41+
type: 'string',
42+
alias: 'o',
43+
describe: 'the path to output that pulled locale messages',
44+
demandOption: true
45+
})
46+
.option('locales', {
47+
type: 'string',
48+
alias: 'l',
49+
default: '',
50+
describe: `option for some locales of locale messages, you can also be specified multi locale with comma delimiter. if it's not specified pull all locale messages`
51+
})
52+
.option('dryRun', {
53+
type: 'boolean',
54+
alias: 'd',
55+
default: false,
56+
describe: 'run the pull command, but do not pull to locale messages of localization service'
57+
})
58+
}
59+
60+
export const handler = async (args: Arguments<PullOptions>): Promise<unknown> => {
61+
const ProviderFactory = loadProvider(args.provider)
62+
63+
if (ProviderFactory === null) {
64+
// TODO: should refactor console message
65+
console.log(`Not found ${args.provider} provider`)
66+
return
67+
}
68+
69+
if (!args.output) {
70+
// TODO: should refactor console message
71+
console.log('You need to specify --output')
72+
return
73+
}
74+
75+
let conf = DEFUALT_CONF
76+
if (args.conf) {
77+
conf = loadProviderConf(resolve(args.conf))
78+
}
79+
80+
try {
81+
const locales = args.locales?.split(',').filter(p => p) as Locale[] || []
82+
const provider = ProviderFactory(conf)
83+
const resource = await provider.pull(locales, args.dryRun)
84+
await applyPullResource(args.output, resource, args.dryRun)
85+
// TODO: should refactor console message
86+
console.log('pull success')
87+
} catch (e) {
88+
console.error('pull fail', e)
89+
return
90+
}
91+
}
92+
93+
async function applyPullResource (output: string, resource: ProviderPullResource, dryRun: boolean) {
94+
const locales = Object.keys(resource) as Locale[]
95+
debug('applyPullResource', resource, locales, dryRun)
96+
// wrap mkdir with dryRun
97+
const mkdir = async (output: string) => {
98+
return !dryRun
99+
? mkdirPromisify(path.resolve(output), { recursive: true })
100+
: Promise.resolve()
101+
}
102+
103+
// wrap writeFile with dryRun
104+
const writeFile = async (output: string, locale: Locale, message: LocaleMessage) => {
105+
const localePath = path.resolve(output, `${locale}.json`)
106+
console.log(`write '${locale}' messages to ${localePath}`)
107+
return !dryRun
108+
? writeFilePromisify(localePath, JSON.stringify(message, null, 2))
109+
: Promise.resolve()
110+
}
111+
112+
// run!
113+
await mkdir(output)
114+
for (const locale of locales) {
115+
await writeFile(output, locale, resource[locale])
116+
}
117+
}
118+
119+
export default {
120+
command,
121+
aliases,
122+
describe,
123+
builder,
124+
handler
125+
}

src/commands/push.ts

Lines changed: 1 addition & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,13 @@
11
import { Arguments, Argv } from 'yargs'
22

3-
import { resolve } from '../utils'
3+
import { resolve, loadProvider, loadProviderConf, DEFUALT_CONF } from '../utils'
44
import path from 'path'
55
import glob from 'glob'
66

77
import { debug as Debug } from 'debug'
88
const debug = Debug('vue-i18n-locale-message:commands:push')
99

1010
import {
11-
ProviderFactory,
12-
ProviderConfiguration,
1311
ProviderPushResource,
1412
ProviderPushMode
1513
} from '../../types'
@@ -24,8 +22,6 @@ type PushOptions = {
2422
dryRun: boolean
2523
}
2624

27-
const DEFUALT_CONF = { provider: {}, pushMode: 'locale-message' } as ProviderConfiguration
28-
2925
export const command = 'push'
3026
export const aliases = 'ph'
3127
export const describe = 'push locale messages to localization service'
@@ -110,30 +106,6 @@ export const handler = async (args: Arguments<PushOptions>): Promise<unknown> =>
110106
}
111107
}
112108

113-
function loadProvider (provider: string): ProviderFactory | null {
114-
let mod: ProviderFactory | null = null
115-
try {
116-
// TODO: should validate I/F checking & dynamic importing
117-
const m = require(require.resolve(provider))
118-
debug('loaderProvider', m)
119-
if ('__esModule' in m) {
120-
mod = m.default as ProviderFactory
121-
} else {
122-
mod = m as ProviderFactory
123-
}
124-
} catch (e) { }
125-
return mod
126-
}
127-
128-
function loadProviderConf (confPath: string): ProviderConfiguration {
129-
let conf = DEFUALT_CONF
130-
try {
131-
// TODO: should validate I/F checking & dynamic importing
132-
conf = require(confPath) as ProviderConfiguration
133-
} catch (e) { }
134-
return conf
135-
}
136-
137109
function getProviderPushResource (args: Arguments<PushOptions>, mode: ProviderPushMode): ProviderPushResource {
138110
const resource = { mode } as ProviderPushResource
139111
debug(`getProviderPushResource: mode=${mode}`)

src/utils.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { SFCDescriptor } from 'vue-template-compiler'
22
import { SFCFileInfo, FormatOptions } from '../types'
33
import { VueTemplateCompiler } from '@vue/component-compiler-utils/dist/types'
4+
import { ProviderFactory, ProviderConfiguration } from '../types'
45

56
import { parse } from '@vue/component-compiler-utils'
67
import * as compiler from 'vue-template-compiler'
@@ -121,3 +122,29 @@ function resolveGlob (target: string) {
121122
// TODO: async implementation
122123
return glob.sync(`${target}/**/*.vue`)
123124
}
125+
126+
export const DEFUALT_CONF = { provider: {}, pushMode: 'locale-message' } as ProviderConfiguration
127+
128+
export function loadProvider (provider: string): ProviderFactory | null {
129+
let mod: ProviderFactory | null = null
130+
try {
131+
// TODO: should validate I/F checking & dynamic importing
132+
const m = require(require.resolve(provider))
133+
debug('loaderProvider', m)
134+
if ('__esModule' in m) {
135+
mod = m.default as ProviderFactory
136+
} else {
137+
mod = m as ProviderFactory
138+
}
139+
} catch (e) { }
140+
return mod
141+
}
142+
143+
export function loadProviderConf (confPath: string): ProviderConfiguration {
144+
let conf = DEFUALT_CONF
145+
try {
146+
// TODO: should validate I/F checking & dynamic importing
147+
conf = require(confPath) as ProviderConfiguration
148+
} catch (e) { }
149+
return conf
150+
}

test/commands/__mocks__/l10n-service-provider.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ class L10nServiceProvider {
66
async push (resource, dryRun) {
77
return
88
}
9+
10+
async pull (locales, dryRun) {
11+
return Promise.resolve({ ja: {}, en: {}})
12+
}
913
}
1014

1115
module.exports = L10nServiceProvider

test/commands/pull.test.ts

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
import * as yargs from 'yargs'
2+
import * as path from 'path'
3+
4+
// ------
5+
// mocks
6+
7+
// l10n service provider module
8+
const mockPull = jest.fn()
9+
jest.mock('@scope/l10n-service-provider', () => {
10+
return jest.fn().mockImplementation(() => {
11+
return { pull: mockPull }
12+
})
13+
})
14+
import L10nServiceProvider from '@scope/l10n-service-provider'
15+
16+
// fs module
17+
jest.mock('fs', () => ({
18+
...jest.requireActual('fs'),
19+
mkdir: jest.fn(),
20+
writeFile: jest.fn()
21+
}))
22+
import fs from 'fs'
23+
24+
// --------------------
25+
// setup/teadown hooks
26+
27+
let spyLog
28+
let spyError
29+
beforeEach(() => {
30+
spyLog = jest.spyOn(global.console, 'log')
31+
spyError = jest.spyOn(global.console, 'error')
32+
})
33+
34+
afterEach(() => {
35+
spyError.mockRestore()
36+
spyLog.mockRestore()
37+
jest.clearAllMocks()
38+
})
39+
40+
// -----------
41+
// test cases
42+
43+
test('require options', async () => {
44+
const pull = await import('../../src/commands/pull')
45+
const cmd = yargs.command(pull)
46+
try {
47+
await new Promise((resolve, reject) => {
48+
cmd.parse(`pull`, (err, argv, output) => {
49+
err ? reject(err) : resolve(output)
50+
})
51+
})
52+
} catch (e) {
53+
expect(e).toMatchObject({ name: 'YError' })
54+
}
55+
})
56+
57+
test('--provider: not found', async () => {
58+
const pull = await import('../../src/commands/pull')
59+
const cmd = yargs.command(pull)
60+
await new Promise((resolve, reject) => {
61+
cmd.parse(`pull --provider=./404-provider.js \
62+
--output=./foo`, (err, argv, output) => {
63+
err ? reject(err) : resolve(output)
64+
})
65+
})
66+
expect(spyLog).toHaveBeenCalledWith('Not found ./404-provider.js provider')
67+
})
68+
69+
test('--conf option', async () => {
70+
// setup mocks
71+
mockPull.mockImplementation(locales => Promise.resolve({ ja: {}, en: {}}))
72+
73+
// run
74+
const pull = await import('../../src/commands/pull')
75+
const cmd = yargs.command(pull)
76+
await new Promise((resolve, reject) => {
77+
cmd.parse(`pull --provider=@scope/l10n-service-provider \
78+
--conf=./test/fixtures/conf/l10n-service-provider-conf.json \
79+
--output=./test/fixtures/locales \
80+
--dry-run`, (err, argv, output) => {
81+
err ? reject(err) : resolve(output)
82+
})
83+
})
84+
85+
expect(L10nServiceProvider).toHaveBeenCalledWith({
86+
provider: { token: 'xxx' },
87+
pushMode: 'file-path'
88+
})
89+
})
90+
91+
test('--locales option', async () => {
92+
// setup mocks
93+
mockPull.mockImplementation(locales => Promise.resolve({ ja: {}, en: {}}))
94+
95+
// run
96+
const pull = await import('../../src/commands/pull')
97+
const cmd = yargs.command(pull)
98+
await new Promise((resolve, reject) => {
99+
cmd.parse(`pull --provider=@scope/l10n-service-provider \
100+
--output=./test/fixtures/locales \
101+
--locales=en,ja,fr \
102+
--dry-run`, (err, argv, output) => {
103+
err ? reject(err) : resolve(output)
104+
})
105+
})
106+
107+
expect(mockPull).toHaveBeenCalledWith(['en', 'ja', 'fr'], true)
108+
})
109+
110+
test('--output option', async () => {
111+
// setup mocks
112+
mockPull.mockImplementation(locales => Promise.resolve({ ja: { hello: 'hello' }, en: { hello: 'こんにちわわわ!' }}))
113+
const mockFS = fs as jest.Mocked<typeof fs>
114+
mockFS.mkdir.mockImplementation((p, option, cb) => cb(null))
115+
mockFS.writeFile.mockImplementation((p, data, cb) => cb(null))
116+
117+
// run
118+
const OUTPUT_FULL_PATH = path.resolve('./test/fixtures/locales')
119+
const pull = await import('../../src/commands/pull')
120+
const cmd = yargs.command(pull)
121+
await new Promise((resolve, reject) => {
122+
cmd.parse(`pull --provider=@scope/l10n-service-provider \
123+
--conf=./test/fixtures/conf/l10n-service-provider-conf.json \
124+
--output=./test/fixtures/locales`, (err, argv, output) => {
125+
err ? reject(err) : resolve(output)
126+
})
127+
})
128+
129+
expect(mockFS.mkdir.mock.calls[0][0]).toEqual(OUTPUT_FULL_PATH)
130+
expect(mockFS.mkdir.mock.calls[0][1]).toEqual({ recursive: true })
131+
})

types/index.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,8 +170,11 @@ export type ProviderPushResource = {
170170
messages?: LocaleMessages
171171
}
172172

173+
export type ProviderPullResource = LocaleMessages
174+
173175
export interface Provider {
174176
push (resource: ProviderPushResource, dryRun: boolean): Promise<void>
177+
pull (locales: Locale[], dryRun: boolean): Promise<ProviderPullResource>
175178
}
176179

177180
export type ProviderFactory<T = {}> = (configration: ProviderConfiguration<T>) => Provider

0 commit comments

Comments
 (0)