Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(bundle-utils): support strictly message transform #247

Merged
merged 1 commit into from Mar 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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')
})