From 4a1da0d0734d4d6676437bdde6d3f3b5cfc53e9b Mon Sep 17 00:00:00 2001 From: kazuya kawaguchi Date: Wed, 22 Mar 2023 18:57:11 +0900 Subject: [PATCH] feat(bundle-utils): support strictly message transpilation --- packages/bundle-utils/src/codegen.ts | 42 +++++++++++++--- packages/bundle-utils/src/js.ts | 4 ++ packages/bundle-utils/src/json.ts | 4 ++ packages/bundle-utils/src/yaml.ts | 6 ++- .../test/fixtures/codegen/html.js | 4 ++ .../test/fixtures/codegen/html.json | 4 ++ .../test/fixtures/codegen/html.yaml | 2 + .../__snapshots__/codegen.test.ts.snap | 6 +++ .../generator/__snapshots__/js.test.ts.snap | 22 +++++++++ .../generator/__snapshots__/json.test.ts.snap | 22 +++++++++ .../generator/__snapshots__/yaml.test.ts.snap | 22 +++++++++ .../test/generator/codegen.test.ts | 48 +++++++++++++++++++ .../bundle-utils/test/generator/js.test.ts | 17 +++++++ .../bundle-utils/test/generator/json.test.ts | 17 +++++++ .../bundle-utils/test/generator/yaml.test.ts | 17 +++++++ 15 files changed, 230 insertions(+), 7 deletions(-) create mode 100644 packages/bundle-utils/test/fixtures/codegen/html.js create mode 100644 packages/bundle-utils/test/fixtures/codegen/html.json create mode 100644 packages/bundle-utils/test/fixtures/codegen/html.yaml diff --git a/packages/bundle-utils/src/codegen.ts b/packages/bundle-utils/src/codegen.ts index 2a34c9f..84502dc 100644 --- a/packages/bundle-utils/src/codegen.ts +++ b/packages/bundle-utils/src/codegen.ts @@ -3,7 +3,8 @@ import { baseCompile, CompileError, ResourceNode, - CompileOptions + CompileOptions, + detectHtmlTag } from '@intlify/message-compiler' import { SourceMapGenerator, @@ -11,6 +12,7 @@ import { MappedPosition, MappingItem } from 'source-map' +import { format, escapeHtml as sanitizeHtml, isBoolean } from '@intlify/shared' import type { RawSourceMap } from 'source-map' @@ -53,6 +55,8 @@ export interface CodeGenOptions { forceStringify?: boolean useClassComponent?: boolean allowDynamic?: boolean + strictMessage?: boolean + escapeHtml?: boolean onWarn?: (msg: string) => void onError?: ( msg: string, @@ -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 { 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['onError']>[1] = { source: msg, - path: path ? path.join('.') : '', + path: parsePath(path), code: err.code, domain: err.domain, location: err.location @@ -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( diff --git a/packages/bundle-utils/src/js.ts b/packages/bundle-utils/src/js.ts index 62488ed..6f29166 100644 --- a/packages/bundle-utils/src/js.ts +++ b/packages/bundle-utils/src/js.ts @@ -32,6 +32,8 @@ export function generate( env = 'development', forceStringify = false, onError = undefined, + strictMessage = true, + escapeHtml = false, useClassComponent = false, allowDynamic = false }: CodeGenOptions, @@ -55,6 +57,8 @@ export function generate( filename, forceStringify, onError, + strictMessage, + escapeHtml, useClassComponent } as CodeGenOptions const generator = createCodeGenerator(options) diff --git a/packages/bundle-utils/src/json.ts b/packages/bundle-utils/src/json.ts index c22d9d1..795fe13 100644 --- a/packages/bundle-utils/src/json.ts +++ b/packages/bundle-utils/src/json.ts @@ -38,6 +38,8 @@ export function generate( env = 'development', forceStringify = false, onError = undefined, + strictMessage = true, + escapeHtml = false, useClassComponent = false }: CodeGenOptions, injector?: () => string @@ -63,6 +65,8 @@ export function generate( filename, forceStringify, onError, + strictMessage, + escapeHtml, useClassComponent } as CodeGenOptions const generator = createCodeGenerator(options) diff --git a/packages/bundle-utils/src/yaml.ts b/packages/bundle-utils/src/yaml.ts index e2720e7..5e7fe0d 100644 --- a/packages/bundle-utils/src/yaml.ts +++ b/packages/bundle-utils/src/yaml.ts @@ -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 { @@ -59,6 +61,8 @@ export function generate( filename, forceStringify, onError, + strictMessage, + escapeHtml, useClassComponent } as CodeGenOptions const generator = createCodeGenerator(options) diff --git a/packages/bundle-utils/test/fixtures/codegen/html.js b/packages/bundle-utils/test/fixtures/codegen/html.js new file mode 100644 index 0000000..df983f1 --- /dev/null +++ b/packages/bundle-utils/test/fixtures/codegen/html.js @@ -0,0 +1,4 @@ +export default { + hi: '

hi there!

', + alert: "" +} diff --git a/packages/bundle-utils/test/fixtures/codegen/html.json b/packages/bundle-utils/test/fixtures/codegen/html.json new file mode 100644 index 0000000..09b59a5 --- /dev/null +++ b/packages/bundle-utils/test/fixtures/codegen/html.json @@ -0,0 +1,4 @@ +{ + "hi": "

hi there!

", + "alert": "" +} diff --git a/packages/bundle-utils/test/fixtures/codegen/html.yaml b/packages/bundle-utils/test/fixtures/codegen/html.yaml new file mode 100644 index 0000000..f9f0099 --- /dev/null +++ b/packages/bundle-utils/test/fixtures/codegen/html.yaml @@ -0,0 +1,2 @@ +hi:

hi there!

+alert: diff --git a/packages/bundle-utils/test/generator/__snapshots__/codegen.test.ts.snap b/packages/bundle-utils/test/generator/__snapshots__/codegen.test.ts.snap index 3a46f9f..0c5891e 100644 --- a/packages/bundle-utils/test/generator/__snapshots__/codegen.test.ts.snap +++ b/packages/bundle-utils/test/generator/__snapshots__/codegen.test.ts.snap @@ -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 [ @@ -77,6 +78,10 @@ Object { } `; +exports[`generateMessageFunction escapeHtml default: should not be escaped 1`] = `"(()=>{const fn=(ctx) => {const { normalize: _normalize } = ctx;return _normalize([\\"

hello

\\"])};fn.source=\\"

hello

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

hello

\\";return fn;})()"`; + exports[`generateMessageFunction production 1`] = ` Object { "ast": Object { @@ -138,6 +143,7 @@ Object { "type": 0, }, "code": "(ctx) => {const { normalize: _normalize } = ctx;return _normalize([\\"hello\\"])}", + "errors": Array [], "map": undefined, } `; diff --git a/packages/bundle-utils/test/generator/__snapshots__/js.test.ts.snap b/packages/bundle-utils/test/generator/__snapshots__/js.test.ts.snap index d8e444c..f93f869 100644 --- a/packages/bundle-utils/test/generator/__snapshots__/js.test.ts.snap +++ b/packages/bundle-utils/test/generator/__snapshots__/js.test.ts.snap @@ -461,6 +461,28 @@ Object { } `; +exports[`html tag in message: code 1`] = ` +"{ + \\"hi\\": (()=>{const fn=(ctx) => {const { normalize: _normalize } = ctx;return _normalize([\\"<p>hi there!</p>\\"])};fn.source=\\"

hi there!

\\";return fn;})(), + \\"alert\\": (()=>{const fn=(ctx) => {const { normalize: _normalize } = ctx;return _normalize([\\"<script>window.alert('hi there!')</script>\\"])};fn.source=\\"\\";return fn;})() +}" +`; + +exports[`html tag in message: errors 1`] = ` +Array [ + Object { + "msg": "Detected HTML in '

hi there!

' message.", + "path": "hi", + "source": "

hi there!

", + }, + Object { + "msg": "Detected HTML in '' message.", + "path": "alert", + "source": "", + }, +] +`; + exports[`invalid message syntax: code 1`] = ` "{ \\"hello\\": (()=>{const fn=(ctx) => {const { normalize: _normalize } = ctx;return _normalize([\\"こんにちは\\"])};fn.source=\\"こんにちは\\";return fn;})(), diff --git a/packages/bundle-utils/test/generator/__snapshots__/json.test.ts.snap b/packages/bundle-utils/test/generator/__snapshots__/json.test.ts.snap index 5ffa078..9cf2eb2 100644 --- a/packages/bundle-utils/test/generator/__snapshots__/json.test.ts.snap +++ b/packages/bundle-utils/test/generator/__snapshots__/json.test.ts.snap @@ -540,6 +540,28 @@ Object { } `; +exports[`html tag in message: code 1`] = ` +"{ + \\"hi\\": (()=>{const fn=(ctx) => {const { normalize: _normalize } = ctx;return _normalize([\\"<p>hi there!</p>\\"])};fn.source=\\"

hi there!

\\";return fn;})(), + \\"alert\\": (()=>{const fn=(ctx) => {const { normalize: _normalize } = ctx;return _normalize([\\"<script>window.alert('hi there!')</script>\\"])};fn.source=\\"\\";return fn;})() +}" +`; + +exports[`html tag in message: errors 1`] = ` +Array [ + Object { + "msg": "Detected HTML in '

hi there!

' message.", + "path": "hi", + "source": "

hi there!

", + }, + Object { + "msg": "Detected HTML in '' message.", + "path": "alert", + "source": "", + }, +] +`; + exports[`invalid message syntax: code 1`] = ` "{ \\"hello\\": (()=>{const fn=(ctx) => {const { normalize: _normalize } = ctx;return _normalize([\\"こんにちは\\"])};fn.source=\\"こんにちは\\";return fn;})(), diff --git a/packages/bundle-utils/test/generator/__snapshots__/yaml.test.ts.snap b/packages/bundle-utils/test/generator/__snapshots__/yaml.test.ts.snap index 71580ce..babf1ee 100644 --- a/packages/bundle-utils/test/generator/__snapshots__/yaml.test.ts.snap +++ b/packages/bundle-utils/test/generator/__snapshots__/yaml.test.ts.snap @@ -380,6 +380,28 @@ complex: } `; +exports[`html tag in message: code 1`] = ` +"{ + \\"hi\\": (()=>{const fn=(ctx) => {const { normalize: _normalize } = ctx;return _normalize([\\"<p>hi there!</p>\\"])};fn.source=\\"

hi there!

\\";return fn;})(), + \\"alert\\": (()=>{const fn=(ctx) => {const { normalize: _normalize } = ctx;return _normalize([\\"<script>window.alert('hi there!')</script>\\"])};fn.source=\\"\\";return fn;})() +}" +`; + +exports[`html tag in message: errors 1`] = ` +Array [ + Object { + "msg": "Detected HTML in '

hi there!

' message.", + "path": "hi", + "source": "

hi there!

", + }, + Object { + "msg": "Detected HTML in '' message.", + "path": "alert", + "source": "", + }, +] +`; + exports[`invalid message syntax: code 1`] = ` "{ \\"hello\\": (()=>{const fn=(ctx) => {const { normalize: _normalize } = ctx;return _normalize([\\"こんにちは\\"])};fn.source=\\"こんにちは\\";return fn;})(), diff --git a/packages/bundle-utils/test/generator/codegen.test.ts b/packages/bundle-utils/test/generator/codegen.test.ts index dda616e..d7f4f9e 100644 --- a/packages/bundle-utils/test/generator/codegen.test.ts +++ b/packages/bundle-utils/test/generator/codegen.test.ts @@ -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(`

hello

`, { + onError(msg) { + errors.push(msg) + } + }) + expect(errors[0]).toBe(`Detected HTML in '

hello

' message.`) + }) + + test('false: should not be checked', () => { + const errors: string[] = [] + generateMessageFunction(`

hello

`, { + strictMessage: false, + onError(msg) { + errors.push(msg) + } + }) + expect(errors.length).toBe(0) + }) + }) + + describe('escapeHtml', () => { + test('default: should not be escaped', () => { + const { code } = generateMessageFunction(`

hello

`, {}) + expect(code).toMatchSnapshot() + }) + + test('true: should be escaped', () => { + const { code } = generateMessageFunction(`

hello

`, { + escapeHtml: true + }) + expect(code).toMatchSnapshot() + }) + }) }) diff --git a/packages/bundle-utils/test/generator/js.test.ts b/packages/bundle-utils/test/generator/js.test.ts index 8079142..c7491ad 100644 --- a/packages/bundle-utils/test/generator/js.test.ts +++ b/packages/bundle-utils/test/generator/js.test.ts @@ -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' diff --git a/packages/bundle-utils/test/generator/json.test.ts b/packages/bundle-utils/test/generator/json.test.ts index 6bdb201..75f36e5 100644 --- a/packages/bundle-utils/test/generator/json.test.ts +++ b/packages/bundle-utils/test/generator/json.test.ts @@ -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') +}) diff --git a/packages/bundle-utils/test/generator/yaml.test.ts b/packages/bundle-utils/test/generator/yaml.test.ts index 3ebc5a6..1af57d4 100644 --- a/packages/bundle-utils/test/generator/yaml.test.ts +++ b/packages/bundle-utils/test/generator/yaml.test.ts @@ -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') +})