diff --git a/.flowconfig b/.flowconfig index ceea7c1..c44edac 100644 --- a/.flowconfig +++ b/.flowconfig @@ -4,8 +4,10 @@ [include] [libs] - - ./scripts/babel-nodes.js +[lints] + [options] + +[strict] diff --git a/package-lock.json b/package-lock.json index 0d8c462..eda9d94 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "babel-plugin-transform-react-pug", - "version": "4.0.0", + "version": "4.1.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -61,6 +61,19 @@ } } }, + "@types/babel-types": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@types/babel-types/-/babel-types-7.0.1.tgz", + "integrity": "sha512-EkcOk09rjhivbovP8WreGRbXW20YRfe/qdgXOGq3it3u3aAOWDRNsQhL/XPAWFF7zhZZ+uR+nT+3b+TCkIap1w==" + }, + "@types/babylon": { + "version": "6.16.2", + "resolved": "https://registry.npmjs.org/@types/babylon/-/babylon-6.16.2.tgz", + "integrity": "sha512-+Jty46mPaWe1VAyZbfvgJM4BAdklLWxrT5tc/RjvCgLrtk6gzRY6AOnoWFv4p6hVxhJshDdr2hGVn56alBp97Q==", + "requires": { + "@types/babel-types": "7.0.1" + } + }, "abab": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/abab/-/abab-1.0.4.tgz", @@ -68,9 +81,9 @@ "dev": true }, "acorn": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", - "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=" + "version": "4.0.13", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", + "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=" }, "acorn-globals": { "version": "4.1.0", @@ -1332,9 +1345,9 @@ } }, "clean-css": { - "version": "3.4.26", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-3.4.26.tgz", - "integrity": "sha1-VTI7NE/zvO5oSi6sgck9+Ppz3us=", + "version": "3.4.28", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-3.4.28.tgz", + "integrity": "sha1-vxlF6C/ICPVWlebd6uwBQA79A/8=", "requires": { "commander": "2.8.1", "source-map": "0.4.4" @@ -1447,12 +1460,41 @@ "dev": true }, "constantinople": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-3.1.0.tgz", - "integrity": "sha1-dWnKqKo/jVk11i4fqW+fcCzYHHk=", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/constantinople/-/constantinople-3.1.2.tgz", + "integrity": "sha512-yePcBqEFhLOqSBtwYOGGS1exHo/s1xjekXiinh4itpNQGCu4KA1euPh1fg07N2wMITZXQkBz75Ntdt1ctGZouw==", "requires": { - "acorn": "3.3.0", - "is-expression": "2.1.0" + "@types/babel-types": "7.0.1", + "@types/babylon": "6.16.2", + "babel-types": "6.26.0", + "babylon": "6.18.0" + }, + "dependencies": { + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "requires": { + "core-js": "2.4.1", + "regenerator-runtime": "0.11.1" + } + }, + "babel-types": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-types/-/babel-types-6.26.0.tgz", + "integrity": "sha1-o7Bz+Uq0nrb6Vc1lInozQ4BjJJc=", + "requires": { + "babel-runtime": "6.26.0", + "esutils": "2.0.2", + "lodash": "4.17.4", + "to-fast-properties": "1.0.3" + } + }, + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" + } } }, "content-type-parser": { @@ -1476,8 +1518,7 @@ "core-js": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.4.1.tgz", - "integrity": "sha1-TekR5mew6ukSTjQlS1OupvxhjT4=", - "dev": true + "integrity": "sha1-TekR5mew6ukSTjQlS1OupvxhjT4=" }, "core-util-is": { "version": "1.0.2", @@ -1806,8 +1847,7 @@ "esutils": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", - "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", - "dev": true + "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=" }, "exec-sh": { "version": "0.2.0", @@ -2119,9 +2159,9 @@ } }, "flow-bin": { - "version": "0.65.0", - "resolved": "https://registry.npmjs.org/flow-bin/-/flow-bin-0.65.0.tgz", - "integrity": "sha512-/Ny7pElDdmwgxq8ALf87/MylzXWAh2Kny1kxGUOG1TxwGEQMctgENtLpuwx8fwvlIUebgJWF8ylhWOcmiNKDpA==", + "version": "0.66.0", + "resolved": "https://registry.npmjs.org/flow-bin/-/flow-bin-0.66.0.tgz", + "integrity": "sha1-qW3ecBXcM0P9VSp7SWPAK+cFyiY=", "dev": true }, "for-in": { @@ -3623,11 +3663,11 @@ } }, "is-expression": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-2.1.0.tgz", - "integrity": "sha1-kb6dR968/vB3l36XIr5tz7RGXvA=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-3.0.0.tgz", + "integrity": "sha1-Oayqa+f9HzRx3ELHQW5hwkMXrJ8=", "requires": { - "acorn": "3.3.0", + "acorn": "4.0.13", "object-assign": "4.1.1" } }, @@ -7006,8 +7046,7 @@ "lodash": { "version": "4.17.4", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.4.tgz", - "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=", - "dev": true + "integrity": "sha1-eCA6TRwyiuHYbcpkYONptX9AVa4=" }, "lodash.sortby": { "version": "4.7.0", @@ -7823,66 +7862,50 @@ "integrity": "sha1-U659nSm7A89WRJOgJhCfVMR/XyY=" }, "pug-filters": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-2.1.2.tgz", - "integrity": "sha1-NZC0Gts0dJ9x0/HxqfdFAREZEXs=", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/pug-filters/-/pug-filters-3.0.1.tgz", + "integrity": "sha1-Fj73O/ux8VRNAysrQPRRMOtS3Ms=", "requires": { - "clean-css": "3.4.26", - "constantinople": "3.1.0", + "clean-css": "3.4.28", + "constantinople": "3.1.2", "jstransformer": "1.0.0", "pug-error": "1.3.2", - "pug-walk": "1.1.2", - "resolve": "1.3.3", + "pug-walk": "1.1.7", + "resolve": "1.5.0", "uglify-js": "2.8.27" } }, "pug-lexer": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-3.1.0.tgz", - "integrity": "sha1-/QhzdtSmdbT1n4/vQiiDQ06VgaI=", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/pug-lexer/-/pug-lexer-4.0.0.tgz", + "integrity": "sha1-IQwYRX7y4XYCQnQMXmR715TOwng=", "requires": { "character-parser": "2.2.0", "is-expression": "3.0.0", "pug-error": "1.3.2" - }, - "dependencies": { - "acorn": { - "version": "4.0.13", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-4.0.13.tgz", - "integrity": "sha1-EFSVrlNh1pe9GVyCUZLhrX8lN4c=" - }, - "is-expression": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-expression/-/is-expression-3.0.0.tgz", - "integrity": "sha1-Oayqa+f9HzRx3ELHQW5hwkMXrJ8=", - "requires": { - "acorn": "4.0.13", - "object-assign": "4.1.1" - } - } } }, "pug-parser": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-4.0.0.tgz", - "integrity": "sha512-ocEUFPdLG9awwFj0sqi1uiZLNvfoodCMULZzkRqILryIWc/UUlDlxqrKhKjAIIGPX/1SNsvxy63+ayEGocGhQg==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pug-parser/-/pug-parser-5.0.0.tgz", + "integrity": "sha1-45Stmz/KkxI5QK/4hcBuRKt+aOQ=", "requires": { "pug-error": "1.3.2", "token-stream": "0.0.1" } }, "pug-strip-comments": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-1.0.2.tgz", - "integrity": "sha1-0xOvoBvMN0mA4TmeI+vy65vchRM=", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pug-strip-comments/-/pug-strip-comments-1.0.3.tgz", + "integrity": "sha1-8VWVkiBu3G+FMQ2s9K+0igJa9Z8=", "requires": { "pug-error": "1.3.2" } }, "pug-walk": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-1.1.2.tgz", - "integrity": "sha1-3rskwHCpXV/Crr81iSYYM9IzLjs=" + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/pug-walk/-/pug-walk-1.1.7.tgz", + "integrity": "sha1-wA1cUSi6xYBr7BXSt+fNq+QlMfM=" }, "punycode": { "version": "1.4.1", @@ -8245,9 +8268,9 @@ "dev": true }, "resolve": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.3.3.tgz", - "integrity": "sha1-ZVkHw0aahoDcLeOidaj91paR8OU=", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.5.0.tgz", + "integrity": "sha512-hgoSGrc3pjzAPHNBg+KnFcK2HwlHTs/YrAGUr6qgTVUZmXv1UEXXl0bZNBKMA9fud6lRYFdPGz0xXxycPzmmiw==", "requires": { "path-parse": "1.0.5" } @@ -8898,8 +8921,7 @@ "to-fast-properties": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-1.0.3.tgz", - "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=", - "dev": true + "integrity": "sha1-uDVx+k2MJbguIxsG46MFXeTKGkc=" }, "to-object-path": { "version": "0.3.0", diff --git a/package.json b/package.json index f4d65d0..f675404 100644 --- a/package.json +++ b/package.json @@ -13,10 +13,10 @@ "babylon": "6.18.0", "common-prefix": "^1.1.0", "pug-error": "^1.3.2", - "pug-filters": "^2.1.2", - "pug-lexer": "^3.1.0", - "pug-parser": "^4.0.0", - "pug-strip-comments": "1.0.2" + "pug-filters": "^3.0.1", + "pug-lexer": "^4.0.0", + "pug-parser": "^5.0.0", + "pug-strip-comments": "^1.0.3" }, "devDependencies": { "babel-cli": "^6.24.1", @@ -25,7 +25,7 @@ "babel-plugin-transform-flow-strip-types": "^6.22.0", "babel-preset-forbeslindesay": "^2.1.0", "babel-types": "^6.24.1", - "flow-bin": "^0.65.0", + "flow-bin": "^0.66.0", "husky": "^0.14.3", "jest": "^22.1.4", "lint-staged": "^7.0.0", @@ -66,4 +66,4 @@ "type": "git", "url": "https://github.com/pugjs/babel-plugin-transform-react-pug.git" } -} \ No newline at end of file +} diff --git a/src/__tests__/__snapshots__/attributes-shorthand.test.js.snap b/src/__tests__/__snapshots__/attributes-shorthand.test.js.snap new file mode 100644 index 0000000..557f0dd --- /dev/null +++ b/src/__tests__/__snapshots__/attributes-shorthand.test.js.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`JavaScript output: transformed source code 1`] = ` +"// To prevent warnings in console from react +const test = 10; + +module.exports =
;" +`; + +exports[`html output: generated html 1`] = ` +
+
+
+`; + +exports[`static html output: static html 1`] = `"
"`; diff --git a/src/__tests__/attributes-shorthand.input.js b/src/__tests__/attributes-shorthand.input.js new file mode 100644 index 0000000..41583a9 --- /dev/null +++ b/src/__tests__/attributes-shorthand.input.js @@ -0,0 +1,13 @@ +// To prevent warnings in console from react +const test = 10; + +module.exports = pug` + div( + data-first + data-second + data-positive=true + data-negative=false + data-check + ) + div(data-one data-two) +`; diff --git a/src/__tests__/attributes-shorthand.test.js b/src/__tests__/attributes-shorthand.test.js new file mode 100644 index 0000000..e7b91de --- /dev/null +++ b/src/__tests__/attributes-shorthand.test.js @@ -0,0 +1,3 @@ +import testHelper from './test-helper'; + +testHelper(__dirname + '/attributes-shorthand.input.js'); diff --git a/src/__tests__/attributes-unescaped.test.js b/src/__tests__/attributes-unescaped.test.js new file mode 100644 index 0000000..b5d901b --- /dev/null +++ b/src/__tests__/attributes-unescaped.test.js @@ -0,0 +1,67 @@ +import React from 'react'; +import {transform} from 'babel-core'; +import renderer from 'react-test-renderer'; +import transformReactPug from '../'; + +const transformationOptions = { + babelrc: false, + plugins: [transformReactPug], +}; + +const transformer = code => { + return transform(`pug\`${code}\``, transformationOptions).code; +}; + +const ExpectedError = /Unescaped attributes/; + +test('throws error when pass string', () => { + const wrapped = () => + transformer(` + div(name!="hello") + `); + + expect(wrapped).toThrowError(ExpectedError); +}); + +test('throws error when pass number', () => { + const wrapped = () => + transformer(` + div(name!=42) + `); + + expect(wrapped).toThrowError(ExpectedError); +}); + +test('throws error when pass variable', () => { + const wrapped = () => + transformer(` + - const variable = 'value' + div(name!=variable.toString()) + `); + + expect(wrapped).toThrowError(ExpectedError); +}); + +test('does not throw error when pass variable or just string', () => { + const wrapped = () => + transformer(` + - const variable = 'value' + div#id.class( + data-string="hello" + data-variable=variable + data-number=42 + ) + div(class=['one', 'two']) + `); + + expect(wrapped).not.toThrowError(ExpectedError); +}); + +test('does not throw error when pass boolean variables', () => { + const wrapped = () => + transformer(` + div(data-first data-second data-third) + `); + + expect(wrapped).not.toThrowError(ExpectedError); +}); diff --git a/src/context.js b/src/context.js index be3db6d..3e6d23a 100644 --- a/src/context.js +++ b/src/context.js @@ -1,6 +1,6 @@ // @flow -import {readFileSync} from 'fs'; +import {readFileSync, existsSync} from 'fs'; import error from 'pug-error'; import type {Key} from './block-key'; import {getCurrentLocation} from './babel-types'; @@ -42,12 +42,17 @@ class Context { } error(code: string, message: string): Error { - const src = readFileSync(this.file.opts.filename, 'utf8'); - return error(code, message, { + const options: Object = { filename: this.file.opts.filename, line: getCurrentLocation().start.line - 1, - src, - }); + src: null, + }; + + if (existsSync(options.filename)) { + options.src = readFileSync(this.file.opts.filename, 'utf8'); + } + + return error(code, message, options); } noKey(fn: (context: Context) => T): T { diff --git a/src/visitors/Tag.js b/src/visitors/Tag.js index 633ce85..353fdcb 100644 --- a/src/visitors/Tag.js +++ b/src/visitors/Tag.js @@ -6,6 +6,14 @@ import t from '../babel-types'; import {visitJsx, visitJsxExpressions} from '../visitors'; import {getInterpolationRefs} from '../utils/interpolation'; +type PugAttribute = { + name: string, + val: string, + mustEscape: boolean, +}; + +type Attribute = JSXAttribute | JSXSpreadAttribute; + /** * Get children nodes from the node, passing the node's * context to the children and generating JSX values. @@ -27,15 +35,12 @@ function getChildren(node: Object, context: Context): Array { * them into JSX attributes. * @param {Object} node - The node * @param {Context} context - The context - * @returns {Array} + * @returns {Array} */ -function getAttributes( - node: Object, - context: Context, -): Array { - const classes = []; - const attrs = node.attrs - .map(({name, val, mustEscape}) => { +function getAttributes(node: Object, context: Context): Array { + const classes: Array = []; + const attrs: Array = node.attrs + .map(({name, val, mustEscape}: PugAttribute): Attribute | null => { if (/\.\.\./.test(name) && val === true) { return t.jSXSpreadAttribute(parseExpression(name.substr(3), context)); } @@ -54,8 +59,16 @@ function getAttributes( const expr = parseExpression(val === true ? 'true' : val, context); - if (!mustEscape && (!t.isStringLiteral(expr) || /(\<\>\&)/.test(val))) { - throw new Error('Unescaped attributes are not supported in react-pug'); + if (!mustEscape) { + const canSkipEscaping = + (name === 'className' || name === 'id') && t.isStringLiteral(expr); + + if (!canSkipEscaping) { + throw context.error( + 'INVALID_EXPRESSION', + 'Unescaped attributes are not supported in react-pug', + ); + } } if (expr == null) { @@ -79,20 +92,22 @@ function getAttributes( return t.jSXAttribute(t.jSXIdentifier(name), jsxValue); }) .filter(Boolean); + if (classes.length) { const value = classes.every(cls => t.isStringLiteral(cls)) ? t.stringLiteral(classes.map(cls => (cls: any).value).join(' ')) : t.jSXExpressionContainer( - t.callExpression( - t.memberExpression( - t.arrayExpression(classes), - t.identifier('join'), + t.callExpression( + t.memberExpression( + t.arrayExpression(classes), + t.identifier('join'), + ), + [t.stringLiteral(' ')], ), - [t.stringLiteral(' ')], - ), - ); + ); attrs.push(t.jSXAttribute(t.jSXIdentifier('className'), value)); } + return attrs; }