Skip to content

Commit

Permalink
feat(bundle-utils): support strictly message transpilation (#247)
Browse files Browse the repository at this point in the history
  • Loading branch information
kazupon committed Mar 22, 2023
1 parent e4a74fe commit e864140
Show file tree
Hide file tree
Showing 15 changed files with 230 additions and 7 deletions.
42 changes: 36 additions & 6 deletions packages/bundle-utils/src/codegen.ts
Expand Up @@ -3,14 +3,16 @@ import {
baseCompile,
CompileError,
ResourceNode,
CompileOptions
CompileOptions,
detectHtmlTag
} from '@intlify/message-compiler'
import {
SourceMapGenerator,
SourceMapConsumer,
MappedPosition,
MappingItem
} from 'source-map'
import { format, escapeHtml as sanitizeHtml, isBoolean } from '@intlify/shared'

import type { RawSourceMap } from 'source-map'

Expand Down Expand Up @@ -53,6 +55,8 @@ export interface CodeGenOptions {
forceStringify?: boolean
useClassComponent?: boolean
allowDynamic?: boolean
strictMessage?: boolean
escapeHtml?: boolean
onWarn?: (msg: string) => void
onError?: (
msg: string,
Expand Down Expand Up @@ -228,20 +232,46 @@ function advancePositionWithSource(
return pos
}

const DETECT_MESSAGE = `Detected HTML in '{msg}' message.`
const ON_ERROR_NOOP = () => {} // eslint-disable-line @typescript-eslint/no-empty-function

function parsePath(path?: string[]): string {
return path ? path.join('.') : ''
}

export function generateMessageFunction(
msg: string,
options: CodeGenOptions,
path?: string[]
): CodeGenResult<ResourceNode> {
const env = options.env != null ? options.env : 'development'
const onError = options.onError
const strictMessage = isBoolean(options.strictMessage)
? options.strictMessage
: true
const escapeHtml = !!options.escapeHtml
const onError = options.onError || ON_ERROR_NOOP
const errors = [] as CompileError[]

let detecteHtmlInMsg = false
if (detectHtmlTag(msg)) {
detecteHtmlInMsg = true
if (strictMessage) {
const errMsg = format(DETECT_MESSAGE, { msg })
onError(format(errMsg), {
source: msg,
path: parsePath(path)
})
}
}

const _msg = detecteHtmlInMsg && escapeHtml ? sanitizeHtml(msg) : msg

const newOptions = Object.assign({ mode: 'arrow' }, options) as CompileOptions
newOptions.onError = (err: CompileError): void => {
if (onError) {
const extra: Parameters<Required<CodeGenOptions>['onError']>[1] = {
source: msg,
path: path ? path.join('.') : '',
path: parsePath(path),
code: err.code,
domain: err.domain,
location: err.location
Expand All @@ -250,14 +280,14 @@ export function generateMessageFunction(
errors.push(err)
}
}
const { code, ast, map } = baseCompile(msg, newOptions)
const { code, ast, map } = baseCompile(_msg, newOptions)
const occured = errors.length > 0
const genCode = !occured
? env === 'development'
? `(()=>{const fn=${code};fn.source=${JSON.stringify(msg)};return fn;})()`
: `${code}`
: msg
return { code: genCode, ast, map }
: _msg
return { code: genCode, ast, map, errors }
}

export function mapLinesColumns(
Expand Down
4 changes: 4 additions & 0 deletions packages/bundle-utils/src/js.ts
Expand Up @@ -32,6 +32,8 @@ export function generate(
env = 'development',
forceStringify = false,
onError = undefined,
strictMessage = true,
escapeHtml = false,
useClassComponent = false,
allowDynamic = false
}: CodeGenOptions,
Expand All @@ -55,6 +57,8 @@ export function generate(
filename,
forceStringify,
onError,
strictMessage,
escapeHtml,
useClassComponent
} as CodeGenOptions
const generator = createCodeGenerator(options)
Expand Down
4 changes: 4 additions & 0 deletions packages/bundle-utils/src/json.ts
Expand Up @@ -38,6 +38,8 @@ export function generate(
env = 'development',
forceStringify = false,
onError = undefined,
strictMessage = true,
escapeHtml = false,
useClassComponent = false
}: CodeGenOptions,
injector?: () => string
Expand All @@ -63,6 +65,8 @@ export function generate(
filename,
forceStringify,
onError,
strictMessage,
escapeHtml,
useClassComponent
} as CodeGenOptions
const generator = createCodeGenerator(options)
Expand Down
6 changes: 5 additions & 1 deletion packages/bundle-utils/src/yaml.ts
Expand Up @@ -38,7 +38,9 @@ export function generate(
sourceMap = false,
env = 'development',
forceStringify = false,
onError = undefined
onError = undefined,
strictMessage = true,
escapeHtml = false
}: CodeGenOptions,
injector?: () => string
): CodeGenResult<YAMLProgram> {
Expand All @@ -59,6 +61,8 @@ export function generate(
filename,
forceStringify,
onError,
strictMessage,
escapeHtml,
useClassComponent
} as CodeGenOptions
const generator = createCodeGenerator(options)
Expand Down
4 changes: 4 additions & 0 deletions packages/bundle-utils/test/fixtures/codegen/html.js
@@ -0,0 +1,4 @@
export default {
hi: '<p>hi there!</p>',
alert: "<script>window.alert('hi there!')</script>"
}
4 changes: 4 additions & 0 deletions packages/bundle-utils/test/fixtures/codegen/html.json
@@ -0,0 +1,4 @@
{
"hi": "<p>hi there!</p>",
"alert": "<script>window.alert('hi there!')</script>"
}
2 changes: 2 additions & 0 deletions packages/bundle-utils/test/fixtures/codegen/html.yaml
@@ -0,0 +1,2 @@
hi: <p>hi there!</p>
alert: <script>window.alert('hi there!')</script>
Expand Up @@ -61,6 +61,7 @@ Object {
"type": 0,
},
"code": "(()=>{const fn=(ctx) => {const { normalize: _normalize } = ctx;return _normalize([\\"hello\\"])};fn.source=\\"hello\\";return fn;})()",
"errors": Array [],
"map": Object {
"mappings": "mEAAAA",
"names": Array [
Expand All @@ -77,6 +78,10 @@ Object {
}
`;

exports[`generateMessageFunction escapeHtml default: should not be escaped 1`] = `"(()=>{const fn=(ctx) => {const { normalize: _normalize } = ctx;return _normalize([\\"<p>hello</p>\\"])};fn.source=\\"<p>hello</p>\\";return fn;})()"`;

exports[`generateMessageFunction escapeHtml true: should be escaped 1`] = `"(()=>{const fn=(ctx) => {const { normalize: _normalize } = ctx;return _normalize([\\"&lt;p&gt;hello&lt;/p&gt;\\"])};fn.source=\\"<p>hello</p>\\";return fn;})()"`;

exports[`generateMessageFunction production 1`] = `
Object {
"ast": Object {
Expand Down Expand Up @@ -138,6 +143,7 @@ Object {
"type": 0,
},
"code": "(ctx) => {const { normalize: _normalize } = ctx;return _normalize([\\"hello\\"])}",
"errors": Array [],
"map": undefined,
}
`;
22 changes: 22 additions & 0 deletions packages/bundle-utils/test/generator/__snapshots__/js.test.ts.snap
Expand Up @@ -461,6 +461,28 @@ Object {
}
`;

exports[`html tag in message: code 1`] = `
"{
\\"hi\\": (()=>{const fn=(ctx) => {const { normalize: _normalize } = ctx;return _normalize([\\"&lt;p&gt;hi there!&lt;/p&gt;\\"])};fn.source=\\"<p>hi there!</p>\\";return fn;})(),
\\"alert\\": (()=>{const fn=(ctx) => {const { normalize: _normalize } = ctx;return _normalize([\\"&lt;script&gt;window.alert(&apos;hi there!&apos;)&lt;/script&gt;\\"])};fn.source=\\"<script>window.alert('hi there!')</script>\\";return fn;})()
}"
`;

exports[`html tag in message: errors 1`] = `
Array [
Object {
"msg": "Detected HTML in '<p>hi there!</p>' message.",
"path": "hi",
"source": "<p>hi there!</p>",
},
Object {
"msg": "Detected HTML in '<script>window.alert('hi there!')</script>' message.",
"path": "alert",
"source": "<script>window.alert('hi there!')</script>",
},
]
`;

exports[`invalid message syntax: code 1`] = `
"{
\\"hello\\": (()=>{const fn=(ctx) => {const { normalize: _normalize } = ctx;return _normalize([\\"こんにちは\\"])};fn.source=\\"こんにちは\\";return fn;})(),
Expand Down
Expand Up @@ -540,6 +540,28 @@ Object {
}
`;
exports[`html tag in message: code 1`] = `
"{
\\"hi\\": (()=>{const fn=(ctx) => {const { normalize: _normalize } = ctx;return _normalize([\\"&lt;p&gt;hi there!&lt;/p&gt;\\"])};fn.source=\\"<p>hi there!</p>\\";return fn;})(),
\\"alert\\": (()=>{const fn=(ctx) => {const { normalize: _normalize } = ctx;return _normalize([\\"&lt;script&gt;window.alert(&apos;hi there!&apos;)&lt;/script&gt;\\"])};fn.source=\\"<script>window.alert('hi there!')</script>\\";return fn;})()
}"
`;
exports[`html tag in message: errors 1`] = `
Array [
Object {
"msg": "Detected HTML in '<p>hi there!</p>' message.",
"path": "hi",
"source": "<p>hi there!</p>",
},
Object {
"msg": "Detected HTML in '<script>window.alert('hi there!')</script>' message.",
"path": "alert",
"source": "<script>window.alert('hi there!')</script>",
},
]
`;
exports[`invalid message syntax: code 1`] = `
"{
\\"hello\\": (()=>{const fn=(ctx) => {const { normalize: _normalize } = ctx;return _normalize([\\"こんにちは\\"])};fn.source=\\"こんにちは\\";return fn;})(),
Expand Down
Expand Up @@ -380,6 +380,28 @@ complex:
}
`;
exports[`html tag in message: code 1`] = `
"{
\\"hi\\": (()=>{const fn=(ctx) => {const { normalize: _normalize } = ctx;return _normalize([\\"&lt;p&gt;hi there!&lt;/p&gt;\\"])};fn.source=\\"<p>hi there!</p>\\";return fn;})(),
\\"alert\\": (()=>{const fn=(ctx) => {const { normalize: _normalize } = ctx;return _normalize([\\"&lt;script&gt;window.alert(&apos;hi there!&apos;)&lt;/script&gt;\\"])};fn.source=\\"<script>window.alert('hi there!')</script>\\";return fn;})()
}"
`;
exports[`html tag in message: errors 1`] = `
Array [
Object {
"msg": "Detected HTML in '<p>hi there!</p>' message.",
"path": "hi",
"source": "<p>hi there!</p>",
},
Object {
"msg": "Detected HTML in '<script>window.alert('hi there!')</script>' message.",
"path": "alert",
"source": "<script>window.alert('hi there!')</script>",
},
]
`;
exports[`invalid message syntax: code 1`] = `
"{
\\"hello\\": (()=>{const fn=(ctx) => {const { normalize: _normalize } = ctx;return _normalize([\\"こんにちは\\"])};fn.source=\\"こんにちは\\";return fn;})(),
Expand Down
48 changes: 48 additions & 0 deletions packages/bundle-utils/test/generator/codegen.test.ts
Expand Up @@ -14,4 +14,52 @@ describe('generateMessageFunction', () => {
generateMessageFunction('hello', { env: 'production' })
).toMatchSnapshot()
})

test('syntax error', () => {
const errors: string[] = []
const { code } = generateMessageFunction(`|`, {
onError(msg) {
errors.push(msg)
}
})
expect(errors.length).toBe(1)
expect(code).toBe(`|`)
})

describe('strictMessage', () => {
test('default: should be checked', () => {
const errors: string[] = []
generateMessageFunction(`<p>hello</p>`, {
onError(msg) {
errors.push(msg)
}
})
expect(errors[0]).toBe(`Detected HTML in '<p>hello</p>' message.`)
})

test('false: should not be checked', () => {
const errors: string[] = []
generateMessageFunction(`<p>hello</p>`, {
strictMessage: false,
onError(msg) {
errors.push(msg)
}
})
expect(errors.length).toBe(0)
})
})

describe('escapeHtml', () => {
test('default: should not be escaped', () => {
const { code } = generateMessageFunction(`<p>hello</p>`, {})
expect(code).toMatchSnapshot()
})

test('true: should be escaped', () => {
const { code } = generateMessageFunction(`<p>hello</p>`, {
escapeHtml: true
})
expect(code).toMatchSnapshot()
})
})
})
17 changes: 17 additions & 0 deletions packages/bundle-utils/test/generator/js.test.ts
Expand Up @@ -184,6 +184,23 @@ test('invalid message syntax', async () => {
expect(map).toMatchSnapshot('map')
})

test('html tag in message', async () => {
const { source } = await readFile('./fixtures/codegen/html.js')
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const errors = [] as any
const { code } = generate(source, {
type: 'bare',
escapeHtml: true,
env: 'development',
onError(msg, extra) {
errors.push(Object.assign({ msg }, extra || {}))
}
})

expect(errors).toMatchSnapshot('errors')
expect(code).toMatchSnapshot('code')
})

test('no export default with object', async () => {
const { source } = await readFile(
'./fixtures/codegen/no-export-default-with-object.js'
Expand Down
17 changes: 17 additions & 0 deletions packages/bundle-utils/test/generator/json.test.ts
Expand Up @@ -185,3 +185,20 @@ test('invalid message syntax', async () => {
expect(code).toMatchSnapshot('code')
expect(map).toMatchSnapshot('map')
})

test('html tag in message', async () => {
const { source } = await readFile('./fixtures/codegen/html.json')
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const errors = [] as any
const { code } = generate(source, {
type: 'bare',
escapeHtml: true,
env: 'development',
onError(msg, extra) {
errors.push(Object.assign({ msg }, extra || {}))
}
})

expect(errors).toMatchSnapshot('errors')
expect(code).toMatchSnapshot('code')
})
17 changes: 17 additions & 0 deletions packages/bundle-utils/test/generator/yaml.test.ts
Expand Up @@ -161,3 +161,20 @@ test('invalid message syntax', async () => {
expect(code).toMatchSnapshot('code')
expect(map).toMatchSnapshot('map')
})

test('html tag in message', async () => {
const { source } = await readFile('./fixtures/codegen/html.yaml')
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const errors = [] as any
const { code } = generate(source, {
type: 'bare',
escapeHtml: true,
env: 'development',
onError(msg, extra) {
errors.push(Object.assign({ msg }, extra || {}))
}
})

expect(errors).toMatchSnapshot('errors')
expect(code).toMatchSnapshot('code')
})

0 comments on commit e864140

Please sign in to comment.