mirror of
https://github.com/cupcakearmy/svelte-i18n.git
synced 2024-11-16 09:59:58 +01:00
test: 💍 add some tests for the cli
This commit is contained in:
parent
60ba73682f
commit
15ed03185a
@ -1,53 +1,35 @@
|
||||
import {
|
||||
Node,
|
||||
ObjectExpression,
|
||||
Property,
|
||||
ImportDeclaration,
|
||||
ImportSpecifier,
|
||||
CallExpression,
|
||||
Identifier,
|
||||
Literal,
|
||||
} from 'estree'
|
||||
import delve from 'dlv'
|
||||
import { walk } from 'estree-walker'
|
||||
import { Ast } from 'svelte/types/compiler/interfaces'
|
||||
import { parse } from 'svelte/compiler'
|
||||
|
||||
import { deepSet } from './includes/deepSet'
|
||||
import { getObjFromExpression } from './includes/getObjFromExpression'
|
||||
import { Message } from './types'
|
||||
|
||||
const LIB_NAME = 'svelte-i18n'
|
||||
const DEFINE_MESSAGES_METHOD_NAME = 'defineMessages'
|
||||
const FORMAT_METHOD_NAMES = new Set(['format', '_'])
|
||||
|
||||
interface Message {
|
||||
node: Node
|
||||
meta: {
|
||||
id?: string
|
||||
default?: string
|
||||
[key: string]: any
|
||||
}
|
||||
}
|
||||
|
||||
const isNumberString = (n: string) => !Number.isNaN(parseInt(n))
|
||||
|
||||
function deepSet(obj: any, path: string, value: any) {
|
||||
const parts = path.replace(/\[(\w+)\]/gi, '.$1').split('.')
|
||||
return parts.reduce((ref, part, i) => {
|
||||
if (part in ref) return (ref = ref[part])
|
||||
|
||||
if (i < parts.length - 1) {
|
||||
if (isNumberString(parts[i + 1])) {
|
||||
return (ref = ref[part] = [])
|
||||
}
|
||||
return (ref = ref[part] = {})
|
||||
}
|
||||
|
||||
return (ref[part] = value)
|
||||
}, obj)
|
||||
}
|
||||
const FORMAT_METHOD_NAMES = new Set(['format', '_', 't'])
|
||||
const IGNORED_UTILITIES = new Set(['number', 'date', 'time'])
|
||||
|
||||
function isFormatCall(node: Node, imports: Set<string>) {
|
||||
if (node.type !== 'CallExpression') return false
|
||||
|
||||
let identifier: Identifier
|
||||
if (node.callee.type === 'MemberExpression') {
|
||||
if (
|
||||
node.callee.type === 'MemberExpression' &&
|
||||
node.callee.property.type === 'Identifier' &&
|
||||
!IGNORED_UTILITIES.has(node.callee.property.name)
|
||||
) {
|
||||
identifier = node.callee.object as Identifier
|
||||
} else if (node.callee.type === 'Identifier') {
|
||||
identifier = node.callee
|
||||
@ -92,22 +74,6 @@ function getFormatSpecifiers(decl: ImportDeclaration) {
|
||||
) as ImportSpecifier[]
|
||||
}
|
||||
|
||||
function getObjFromExpression(exprNode: Node | ObjectExpression) {
|
||||
if (exprNode.type !== 'ObjectExpression') return null
|
||||
return exprNode.properties.reduce<Message>(
|
||||
(acc, prop: Property) => {
|
||||
// we only want primitives
|
||||
if (prop.value.type !== 'Literal') return acc
|
||||
if (prop.value.value !== Object(prop.value.value)) {
|
||||
const key = (prop.key as Identifier).name as string
|
||||
acc.meta[key] = prop.value.value
|
||||
}
|
||||
return acc
|
||||
},
|
||||
{ node: exprNode, meta: {} }
|
||||
)
|
||||
}
|
||||
|
||||
export function collectFormatCalls(ast: Ast) {
|
||||
const importDecls = getLibImportDeclarations(ast)
|
||||
|
||||
@ -122,14 +88,14 @@ export function collectFormatCalls(ast: Ast) {
|
||||
if (imports.size === 0) return []
|
||||
|
||||
const calls: CallExpression[] = []
|
||||
function formatCallsWalker(node: Node) {
|
||||
function enter(node: Node) {
|
||||
if (isFormatCall(node, imports)) {
|
||||
calls.push(node as CallExpression)
|
||||
this.skip()
|
||||
}
|
||||
}
|
||||
walk(ast.instance as any, { enter: formatCallsWalker })
|
||||
walk(ast.html as any, { enter: formatCallsWalker })
|
||||
walk(ast.instance as any, { enter })
|
||||
walk(ast.html as any, { enter })
|
||||
|
||||
return calls
|
||||
}
|
||||
@ -157,7 +123,9 @@ export function collectMessageDefinitions(ast: Ast) {
|
||||
})
|
||||
|
||||
return definitions.flatMap(definitionDict =>
|
||||
definitionDict.properties.map(propNode => propNode.value)
|
||||
definitionDict.properties.map(
|
||||
propNode => propNode.value as ObjectExpression
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
@ -173,19 +141,18 @@ export function collectMessages(markup: string): Message[] {
|
||||
return getObjFromExpression(pathNode)
|
||||
}
|
||||
|
||||
if (pathNode.type !== 'Literal' || typeof pathNode.value !== 'string') {
|
||||
return null
|
||||
}
|
||||
const node = pathNode as Literal
|
||||
const id = node.value as string
|
||||
|
||||
if (options && options.type === 'ObjectExpression') {
|
||||
const messageObj = getObjFromExpression(options)
|
||||
messageObj.meta.id = pathNode.value
|
||||
messageObj.meta.id = id
|
||||
return messageObj
|
||||
}
|
||||
|
||||
return {
|
||||
node: pathNode,
|
||||
meta: { id: pathNode.value },
|
||||
node,
|
||||
meta: { id },
|
||||
}
|
||||
}),
|
||||
].filter(Boolean)
|
||||
|
17
src/cli/includes/deepSet.ts
Normal file
17
src/cli/includes/deepSet.ts
Normal file
@ -0,0 +1,17 @@
|
||||
const isNumberString = (n: string) => !Number.isNaN(parseInt(n))
|
||||
|
||||
export function deepSet(obj: any, path: string, value: any) {
|
||||
const parts = path.replace(/\[(\w+)\]/gi, '.$1').split('.')
|
||||
return parts.reduce((ref, part, i) => {
|
||||
if (part in ref) return (ref = ref[part])
|
||||
|
||||
if (i < parts.length - 1) {
|
||||
if (isNumberString(parts[i + 1])) {
|
||||
return (ref = ref[part] = [])
|
||||
}
|
||||
return (ref = ref[part] = {})
|
||||
}
|
||||
|
||||
return (ref[part] = value)
|
||||
}, obj)
|
||||
}
|
18
src/cli/includes/getObjFromExpression.ts
Normal file
18
src/cli/includes/getObjFromExpression.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { ObjectExpression, Property, Identifier } from 'estree'
|
||||
|
||||
import { Message } from '../types'
|
||||
|
||||
export function getObjFromExpression(exprNode: ObjectExpression) {
|
||||
return exprNode.properties.reduce<Message>(
|
||||
(acc, prop: Property) => {
|
||||
if (prop.value.type !== 'Literal') return acc
|
||||
// we only want primitives
|
||||
if (prop.value.value !== Object(prop.value.value)) {
|
||||
const key = (prop.key as Identifier).name as string
|
||||
acc.meta[key] = prop.value.value
|
||||
}
|
||||
return acc
|
||||
},
|
||||
{ node: exprNode, meta: {} }
|
||||
)
|
||||
}
|
10
src/cli/types/index.ts
Normal file
10
src/cli/types/index.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { Node } from 'estree'
|
||||
|
||||
export interface Message {
|
||||
node: Node
|
||||
meta: {
|
||||
id?: string
|
||||
default?: string
|
||||
[key: string]: any
|
||||
}
|
||||
}
|
@ -10,19 +10,33 @@ import {
|
||||
} from '../../src/cli/extract'
|
||||
|
||||
describe('collecting format calls', () => {
|
||||
it('should return nothing if there are no imports from the library', () => {
|
||||
const ast = parse(`<script>const $_ = () => 0; $_();</script>`)
|
||||
test('returns nothing if there are no imports', () => {
|
||||
const ast = parse(`<script>
|
||||
import Foo from 'foo';
|
||||
const $_ = () => 0; $_();
|
||||
</script>`)
|
||||
const calls = collectFormatCalls(ast)
|
||||
expect(calls).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should collect all format calls in the instance script', () => {
|
||||
test('returns nothing if there are no format imports', () => {
|
||||
const ast = parse(
|
||||
`<script>
|
||||
import { init } from 'svelte-i18n';
|
||||
init({})
|
||||
</script>`
|
||||
)
|
||||
const calls = collectFormatCalls(ast)
|
||||
expect(calls).toHaveLength(0)
|
||||
})
|
||||
|
||||
test('collects all format calls in the instance script', () => {
|
||||
const ast = parse(`<script>
|
||||
import { format, _ } from 'svelte-i18n'
|
||||
$format('foo')
|
||||
format('bar')
|
||||
let label = $_({id:'bar'})
|
||||
const a = { b: () => 0}
|
||||
$format('foo')
|
||||
format('bar')
|
||||
let label = $_({id:'bar'})
|
||||
const a = { b: () => 0}
|
||||
</script>`)
|
||||
const calls = collectFormatCalls(ast)
|
||||
expect(calls).toHaveLength(2)
|
||||
@ -30,24 +44,12 @@ describe('collecting format calls', () => {
|
||||
expect(calls[1]).toMatchObject({ type: 'CallExpression' })
|
||||
})
|
||||
|
||||
it('should collect all format calls with renamed imports', () => {
|
||||
test('collects all format calls with renamed imports', () => {
|
||||
const ast = parse(`<script>
|
||||
import { format as _x, _ as intl } from 'svelte-i18n'
|
||||
$_x('foo')
|
||||
$intl({ id: 'bar' })
|
||||
</script>`)
|
||||
const calls = collectFormatCalls(ast)
|
||||
expect(calls).toHaveLength(2)
|
||||
expect(calls[0]).toMatchObject({ type: 'CallExpression' })
|
||||
expect(calls[1]).toMatchObject({ type: 'CallExpression' })
|
||||
})
|
||||
|
||||
it('should collect all format utility calls', () => {
|
||||
const ast = parse(`<script>
|
||||
import { _ } from 'svelte-i18n'
|
||||
$_.title('foo')
|
||||
$_.capitalize({ id: 'bar' })
|
||||
$_.number(10000)
|
||||
import { format as _x, _ as intl, t as f } from 'svelte-i18n'
|
||||
$_x('foo')
|
||||
$intl({ id: 'bar' })
|
||||
$f({ id: 'bar' })
|
||||
</script>`)
|
||||
const calls = collectFormatCalls(ast)
|
||||
expect(calls).toHaveLength(3)
|
||||
@ -55,24 +57,51 @@ describe('collecting format calls', () => {
|
||||
expect(calls[1]).toMatchObject({ type: 'CallExpression' })
|
||||
expect(calls[2]).toMatchObject({ type: 'CallExpression' })
|
||||
})
|
||||
|
||||
test('collects all string format utility calls', () => {
|
||||
const ast = parse(`<script>
|
||||
import { _ } from 'svelte-i18n'
|
||||
$_.title('foo')
|
||||
$_.capitalize({ id: 'bar' })
|
||||
$_.lower({ id: 'bar' })
|
||||
$_.upper({ id: 'bar' })
|
||||
</script>`)
|
||||
const calls = collectFormatCalls(ast)
|
||||
expect(calls).toHaveLength(4)
|
||||
expect(calls[0]).toMatchObject({ type: 'CallExpression' })
|
||||
expect(calls[1]).toMatchObject({ type: 'CallExpression' })
|
||||
expect(calls[2]).toMatchObject({ type: 'CallExpression' })
|
||||
expect(calls[3]).toMatchObject({ type: 'CallExpression' })
|
||||
})
|
||||
|
||||
test('ignores date, time and number calls', () => {
|
||||
const ast = parse(`<script>
|
||||
import { _ } from 'svelte-i18n'
|
||||
$_.number(1000)
|
||||
$_.date(new Date())
|
||||
$_.time(new Date())
|
||||
</script>`)
|
||||
const calls = collectFormatCalls(ast)
|
||||
expect(calls).toHaveLength(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('collecting message definitions', () => {
|
||||
it('should return nothing if there are no imports from the library', () => {
|
||||
test('returns nothing if there are no imports from the library', () => {
|
||||
const ast = parse(
|
||||
`<script>
|
||||
import foo from 'foo';
|
||||
import { dictionary } from 'svelte-i18n';
|
||||
</script>`,
|
||||
import foo from 'foo';
|
||||
import { dictionary } from 'svelte-i18n';
|
||||
</script>`
|
||||
)
|
||||
expect(collectMessageDefinitions(ast)).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should get all message definition objects', () => {
|
||||
test('gets all message definition objects', () => {
|
||||
const ast = parse(`<script>
|
||||
import { defineMessages } from 'svelte-i18n';
|
||||
defineMessages({ foo: { id: 'foo' }, bar: { id: 'bar' } })
|
||||
defineMessages({ baz: { id: 'baz' }, quix: { id: 'qux' } })
|
||||
import { defineMessages } from 'svelte-i18n';
|
||||
defineMessages({ foo: { id: 'foo' }, bar: { id: 'bar' } })
|
||||
defineMessages({ baz: { id: 'baz' }, quix: { id: 'qux' } })
|
||||
</script>`)
|
||||
const definitions = collectMessageDefinitions(ast)
|
||||
expect(definitions).toHaveLength(4)
|
||||
@ -84,10 +113,11 @@ describe('collecting message definitions', () => {
|
||||
})
|
||||
|
||||
describe('collecting messages', () => {
|
||||
it('should collect all messages in both instance and html ASTs', () => {
|
||||
test('collects all messages in both instance and html ASTs', () => {
|
||||
const markup = `
|
||||
<script>
|
||||
import { _, defineMessages } from 'svelte-i18n';
|
||||
|
||||
console.log($_({ id: 'foo' }))
|
||||
console.log($_.title({ id: 'page.title' }))
|
||||
|
||||
@ -96,10 +126,13 @@ describe('collecting messages', () => {
|
||||
disabled: { id: 'disabled', default: 'Disabled' }
|
||||
})
|
||||
</script>
|
||||
|
||||
<div>{$_('msg_1')}</div>
|
||||
<div>{$_({id: 'msg_2'})}</div>
|
||||
<div>{$_('msg_3', { default: 'Message'})}</div>`
|
||||
|
||||
const messages = collectMessages(markup)
|
||||
|
||||
expect(messages).toHaveLength(7)
|
||||
expect(messages).toEqual(
|
||||
expect.arrayContaining([
|
||||
@ -114,15 +147,16 @@ describe('collecting messages', () => {
|
||||
expect.objectContaining({
|
||||
meta: { id: 'enabled', default: 'Enabled' },
|
||||
}),
|
||||
]),
|
||||
])
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('messages extraction', () => {
|
||||
it('should return a object built based on all found message paths', () => {
|
||||
const markup = `
|
||||
<script>import { _ } from 'svelte-i18n';</script>
|
||||
test('returns a object built based on all found message paths', () => {
|
||||
const markup = `<script>
|
||||
import { _ } from 'svelte-i18n';
|
||||
</script>
|
||||
|
||||
<h1>{$_.title('title')}</h1>
|
||||
<h2>{$_({ id: 'subtitle'})}</h2>
|
||||
@ -131,16 +165,16 @@ describe('messages extraction', () => {
|
||||
expect(dict).toMatchObject({ title: '', subtitle: '' })
|
||||
})
|
||||
|
||||
it('creates deep nested properties', () => {
|
||||
test('creates deep nested properties', () => {
|
||||
const markup = `
|
||||
<script>import { _ } from 'svelte-i18n';</script>
|
||||
|
||||
<h1>{$_.title('home.page.title')}</h1>
|
||||
<h2>{$_({ id: 'home.page.subtitle'})}</h2>
|
||||
<ul>
|
||||
<li>{$_('list[0]')}</li>
|
||||
<li>{$_('list[1]')}</li>
|
||||
<li>{$_('list[2]')}</li>
|
||||
<li>{$_('list.0')}</li>
|
||||
<li>{$_('list.1')}</li>
|
||||
<li>{$_('list.2')}</li>
|
||||
</ul>
|
||||
`
|
||||
const dict = extractMessages(markup)
|
||||
@ -150,38 +184,38 @@ describe('messages extraction', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('creates a shallow dictionary', () => {
|
||||
test('creates a shallow dictionary', () => {
|
||||
const markup = `
|
||||
<script>import { _ } from 'svelte-i18n';</script>
|
||||
|
||||
<h1>{$_.title('home.page.title')}</h1>
|
||||
<h2>{$_({ id: 'home.page.subtitle'})}</h2>
|
||||
<ul>
|
||||
<li>{$_('list[0]')}</li>
|
||||
<li>{$_('list[1]')}</li>
|
||||
<li>{$_('list[2]')}</li>
|
||||
<li>{$_('list.0')}</li>
|
||||
<li>{$_('list.1')}</li>
|
||||
<li>{$_('list.2')}</li>
|
||||
</ul>
|
||||
`
|
||||
const dict = extractMessages(markup, { shallow: true })
|
||||
expect(dict).toMatchObject({
|
||||
'home.page.title': '',
|
||||
'home.page.subtitle': '',
|
||||
'list[0]': '',
|
||||
'list[1]': '',
|
||||
'list[2]': '',
|
||||
'list.0': '',
|
||||
'list.1': '',
|
||||
'list.2': '',
|
||||
})
|
||||
})
|
||||
|
||||
it('allow to pass a initial dictionary and only append non-existing props', () => {
|
||||
test('allow to pass a initial dictionary and only append non-existing props', () => {
|
||||
const markup = `
|
||||
<script>import { _ } from 'svelte-i18n';</script>
|
||||
|
||||
<h1>{$_.title('home.page.title')}</h1>
|
||||
<h2>{$_({ id: 'home.page.subtitle'})}</h2>
|
||||
<ul>
|
||||
<li>{$_('list[0]')}</li>
|
||||
<li>{$_('list[1]')}</li>
|
||||
<li>{$_('list[2]')}</li>
|
||||
<li>{$_('list.0')}</li>
|
||||
<li>{$_('list.1')}</li>
|
||||
<li>{$_('list.2')}</li>
|
||||
</ul>
|
||||
`
|
||||
const dict = extractMessages(markup, {
|
||||
@ -205,7 +239,7 @@ describe('messages extraction', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('allow to pass a initial dictionary and only append shallow non-existing props', () => {
|
||||
test('allow to pass a initial dictionary and only append shallow non-existing props', () => {
|
||||
const markup = `
|
||||
<script>import { _ } from 'svelte-i18n';</script>
|
||||
|
Loading…
Reference in New Issue
Block a user