UNPKG

next-csrf

Version:

CSRF mitigation library for Next.js

620 lines (498 loc) 22.4 kB
"use strict"; exports.__esModule = true; exports.log = log; exports.setStateOptions = exports.createReactComponentImportDeclaration = exports.booleanOption = exports.processCss = exports.combinePlugins = exports.addSourceMaps = exports.makeSourceMapGenerator = exports.makeStyledJsxTag = exports.cssToBabelType = exports.templateLiteralFromPreprocessedCss = exports.computeClassNames = exports.getJSXStyleInfo = exports.validateExternalExpressions = exports.isDynamic = exports.validateExpressionVisitor = exports.findStyles = exports.isStyledJsx = exports.isGlobalEl = exports.getScope = exports.addClassName = exports.hashString = void 0; var _path = _interopRequireDefault(require("path")); var t = _interopRequireWildcard(require("@babel/types")); var _stringHash = _interopRequireDefault(require("string-hash")); var _sourceMap = require("source-map"); var _convertSourceMap = _interopRequireDefault(require("convert-source-map")); var _styleTransform = _interopRequireDefault(require("./lib/style-transform")); var _constants = require("./_constants"); function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function _getRequireWildcardCache() { return cache; }; return cache; } function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { "default": obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj["default"] = obj; if (cache) { cache.set(obj, newObj); } return newObj; } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { "default": obj }; } function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); } var concat = function concat(a, b) { return t.binaryExpression('+', a, b); }; var and = function and(a, b) { return t.logicalExpression('&&', a, b); }; var or = function or(a, b) { return t.logicalExpression('||', a, b); }; var joinSpreads = function joinSpreads(spreads) { return spreads.reduce(function (acc, curr) { return or(acc, curr); }); }; var hashString = function hashString(str) { return String((0, _stringHash["default"])(str)); }; exports.hashString = hashString; var addClassName = function addClassName(path, jsxId) { var jsxIdWithSpace = concat(jsxId, t.stringLiteral(' ')); var attributes = path.get('attributes'); var spreads = []; var className = null; // Find className and collect spreads for (var i = attributes.length - 1, attr; attr = attributes[i]; i--) { var node = attr.node; if (t.isJSXSpreadAttribute(attr)) { if (t.isObjectExpression(node.argument)) { var properties = node.argument.properties; var index = properties.findIndex(function (property) { return property.key.name === 'className'; }); if (~index) { className = attr.get('argument').get("properties." + index); // Remove jsx spread attribute if there is only className property if (properties.length === 1) { attr.remove(); } break; } } if (t.isMemberExpression(node.argument) || t.isIdentifier(node.argument)) { var name = node.argument.name; var spreadObj = t.isMemberExpression(node.argument) ? node.argument : t.identifier(name); var attrNameDotClassName = t.memberExpression(spreadObj, t.identifier('className')); spreads.push( // `${name} && ${name}.className != null && ${name}.className` and(spreadObj, and(t.binaryExpression('!=', attrNameDotClassName, t.nullLiteral()), attrNameDotClassName))); } continue; } if (t.isJSXAttribute(attr) && node.name.name === 'className') { className = attributes[i]; // found className break the loop break; } } if (className) { var newClassName = className.node.value.expression || className.node.value; newClassName = t.isStringLiteral(newClassName) || t.isTemplateLiteral(newClassName) ? newClassName : or(newClassName, t.stringLiteral('')); className.remove(); className = t.jSXExpressionContainer(spreads.length === 0 ? concat(jsxIdWithSpace, newClassName) : concat(jsxIdWithSpace, or(joinSpreads(spreads), newClassName))); } else { className = t.jSXExpressionContainer(spreads.length === 0 ? jsxId : concat(jsxIdWithSpace, or(joinSpreads(spreads), t.stringLiteral('')))); } path.node.attributes.push(t.jSXAttribute(t.jSXIdentifier('className'), className)); }; exports.addClassName = addClassName; var getScope = function getScope(path) { return (path.findParent(function (path) { return path.isFunctionDeclaration() || path.isArrowFunctionExpression() || path.isClassMethod(); }) || path).scope; }; exports.getScope = getScope; var isGlobalEl = function isGlobalEl(el) { return el && el.attributes.some(function (_ref) { var name = _ref.name; return name && name.name === _constants.GLOBAL_ATTRIBUTE; }); }; exports.isGlobalEl = isGlobalEl; var isStyledJsx = function isStyledJsx(_ref2) { var el = _ref2.node; return t.isJSXElement(el) && el.openingElement.name.name === 'style' && el.openingElement.attributes.some(function (attr) { return attr.name.name === _constants.STYLE_ATTRIBUTE; }); }; exports.isStyledJsx = isStyledJsx; var findStyles = function findStyles(path) { if (isStyledJsx(path)) { var node = path.node; return isGlobalEl(node.openingElement) ? [path] : []; } return path.get('children').filter(isStyledJsx); }; // The following visitor ensures that MemberExpressions and Identifiers // are not in the scope of the current Method (render) or function (Component). exports.findStyles = findStyles; var validateExpressionVisitor = { MemberExpression: function MemberExpression(path, scope) { var node = path.node; if (t.isIdentifier(node.property) && t.isThisExpression(node.object) && (node.property.name === 'props' || node.property.name === 'state' || node.property.name === 'context') || t.isIdentifier(node.object) && scope.hasOwnBinding(node.object.name)) { throw path.buildCodeFrameError("Expected a constant " + "as part of the template literal expression " + "(eg: <style jsx>{`p { color: ${myColor}`}</style>), " + ("but got a MemberExpression: this." + node.property.name)); } }, Identifier: function Identifier(path, scope) { var name = path.node.name; if (t.isMemberExpression(path.parentPath) && scope.hasOwnBinding(name)) { return; } var targetScope = path.scope; var isDynamicBinding = false; // Traversing scope chain in order to find current variable. // If variable has no parent scope and it's `const` then we can interp. it // as static in order to optimize styles. // `let` and `var` can be changed during runtime. while (targetScope) { if (targetScope.hasOwnBinding(name)) { var binding = targetScope.bindings[name]; isDynamicBinding = binding.scope.parent !== null || binding.kind !== 'const'; break; } targetScope = targetScope.parent; } if (isDynamicBinding) { throw path.buildCodeFrameError("Expected `" + name + "` " + "to not come from the closest scope.\n" + "Styled JSX encourages the use of constants " + "instead of `props` or dynamic values " + "which are better set via inline styles or `className` toggling. " + "See https://github.com/zeit/styled-jsx#dynamic-styles"); } } }; // Use `validateExpressionVisitor` to determine whether the `expr`ession has dynamic values. exports.validateExpressionVisitor = validateExpressionVisitor; var isDynamic = function isDynamic(expr, scope) { try { expr.traverse(validateExpressionVisitor, scope); return false; } catch (error) {} return true; }; exports.isDynamic = isDynamic; var validateExternalExpressionsVisitor = { Identifier: function Identifier(path) { if (t.isMemberExpression(path.parentPath)) { return; } var name = path.node.name; if (!path.scope.hasBinding(name)) { throw path.buildCodeFrameError(path.getSource()); } }, MemberExpression: function MemberExpression(path) { var node = path.node; if (!t.isIdentifier(node.object)) { return; } if (!path.scope.hasBinding(node.object.name)) { throw path.buildCodeFrameError(path.getSource()); } }, ThisExpression: function ThisExpression(path) { throw new Error(path.parentPath.getSource()); } }; var validateExternalExpressions = function validateExternalExpressions(path) { try { path.traverse(validateExternalExpressionsVisitor); } catch (error) { throw path.buildCodeFrameError("\n Found an `undefined` or invalid value in your styles: `" + error.message + "`.\n\n If you are trying to use dynamic styles in external files this is unfortunately not possible yet.\n Please put the dynamic parts alongside the component. E.g.\n\n <button>\n <style jsx>{externalStylesReference}</style>\n <style jsx>{`\n button { background-color: ${" + error.message + "} }\n `}</style>\n </button>\n "); } }; exports.validateExternalExpressions = validateExternalExpressions; var getJSXStyleInfo = function getJSXStyleInfo(expr, scope) { var node = expr.node; var location = node.loc; // Assume string literal if (t.isStringLiteral(node)) { return { hash: hashString(node.value), css: node.value, expressions: [], dynamic: false, location: location }; } // Simple template literal without expressions if (node.expressions.length === 0) { return { hash: hashString(node.quasis[0].value.raw), css: node.quasis[0].value.raw, expressions: [], dynamic: false, location: location }; } // Special treatment for template literals that contain expressions: // // Expressions are replaced with a placeholder // so that the CSS compiler can parse and // transform the css source string // without having to know about js literal expressions. // Later expressions are restored. // // e.g. // p { color: ${myConstant}; } // becomes // p { color: %%styled-jsx-placeholder-${id}%%; } var quasis = node.quasis, expressions = node.expressions; var hash = hashString(expr.getSource().slice(1, -1)); var dynamic = scope ? isDynamic(expr, scope) : false; var css = quasis.reduce(function (css, quasi, index) { return "" + css + quasi.value.raw + (quasis.length === index + 1 ? '' : "%%styled-jsx-placeholder-" + index + "%%"); }, ''); return { hash: hash, css: css, expressions: expressions, dynamic: dynamic, location: location }; }; exports.getJSXStyleInfo = getJSXStyleInfo; var computeClassNames = function computeClassNames(styles, externalJsxId) { if (styles.length === 0) { return { className: externalJsxId }; } var hashes = styles.reduce(function (acc, styles) { if (styles.dynamic === false) { acc["static"].push(styles.hash); } else { acc.dynamic.push(styles); } return acc; }, { "static": [], dynamic: [] }); var staticClassName = "jsx-" + hashString(hashes["static"].join(',')); // Static and optionally external classes. E.g. // '[jsx-externalClasses] jsx-staticClasses' if (hashes.dynamic.length === 0) { return { staticClassName: staticClassName, className: externalJsxId ? concat(t.stringLiteral(staticClassName + ' '), externalJsxId) : t.stringLiteral(staticClassName) }; } // _JSXStyle.dynamic([ ['1234', [props.foo, bar, fn(props)]], ... ]) var dynamic = t.callExpression( // Callee: _JSXStyle.dynamic t.memberExpression(t.identifier(_constants.STYLE_COMPONENT), t.identifier('dynamic')), // Arguments [t.arrayExpression(hashes.dynamic.map(function (styles) { return t.arrayExpression([t.stringLiteral(hashString(styles.hash + staticClassName)), t.arrayExpression(styles.expressions)]); }))]); // Dynamic and optionally external classes. E.g. // '[jsx-externalClasses] ' + _JSXStyle.dynamic([ ['1234', [props.foo, bar, fn(props)]], ... ]) if (hashes["static"].length === 0) { return { staticClassName: staticClassName, className: externalJsxId ? concat(concat(externalJsxId, t.stringLiteral(' ')), dynamic) : dynamic }; } // Static, dynamic and optionally external classes. E.g. // '[jsx-externalClasses] jsx-staticClasses ' + _JSXStyle.dynamic([ ['5678', [props.foo, bar, fn(props)]], ... ]) return { staticClassName: staticClassName, className: externalJsxId ? concat(concat(externalJsxId, t.stringLiteral(" " + staticClassName + " ")), dynamic) : concat(t.stringLiteral(staticClassName + " "), dynamic) }; }; exports.computeClassNames = computeClassNames; var templateLiteralFromPreprocessedCss = function templateLiteralFromPreprocessedCss(css, expressions) { var quasis = []; var finalExpressions = []; var parts = css.split(/(?:%%styled-jsx-placeholder-(\d+)%%)/g); if (parts.length === 1) { return t.stringLiteral(css); } parts.forEach(function (part, index) { if (index % 2 > 0) { // This is necessary because, after preprocessing, declarations might have been alterate. // eg. properties are auto prefixed and therefore expressions need to match. finalExpressions.push(expressions[part]); } else { quasis.push(part); } }); return t.templateLiteral(quasis.map(function (quasi, index) { return t.templateElement({ raw: quasi, cooked: quasi }, quasis.length === index + 1); }), finalExpressions); }; exports.templateLiteralFromPreprocessedCss = templateLiteralFromPreprocessedCss; var cssToBabelType = function cssToBabelType(css) { if (typeof css === 'string') { return t.stringLiteral(css); } if (Array.isArray(css)) { return t.arrayExpression(css); } return t.cloneDeep(css); }; exports.cssToBabelType = cssToBabelType; var makeStyledJsxTag = function makeStyledJsxTag(id, transformedCss, expressions) { if (expressions === void 0) { expressions = []; } var css = cssToBabelType(transformedCss); var attributes = [t.jSXAttribute(t.jSXIdentifier(_constants.STYLE_COMPONENT_ID), t.jSXExpressionContainer(typeof id === 'string' ? t.stringLiteral(id) : id))]; if (expressions.length > 0) { attributes.push(t.jSXAttribute(t.jSXIdentifier(_constants.STYLE_COMPONENT_DYNAMIC), t.jSXExpressionContainer(t.arrayExpression(expressions)))); } return t.jSXElement(t.jSXOpeningElement(t.jSXIdentifier(_constants.STYLE_COMPONENT), attributes), t.jSXClosingElement(t.jSXIdentifier(_constants.STYLE_COMPONENT)), [t.jSXExpressionContainer(css)]); }; exports.makeStyledJsxTag = makeStyledJsxTag; var makeSourceMapGenerator = function makeSourceMapGenerator(file) { var filename = file.sourceFileName; var generator = new _sourceMap.SourceMapGenerator({ file: filename, sourceRoot: file.sourceRoot }); generator.setSourceContent(filename, file.code); return generator; }; exports.makeSourceMapGenerator = makeSourceMapGenerator; var addSourceMaps = function addSourceMaps(code, generator, filename) { var sourceMaps = [_convertSourceMap["default"].fromObject(generator).toComment({ multiline: true }), "/*@ sourceURL=" + filename.replace(/\\/g, '\\\\') + " */"]; if (Array.isArray(code)) { return code.concat(sourceMaps); } return [code].concat(sourceMaps).join('\n'); }; exports.addSourceMaps = addSourceMaps; var combinedPluginsCache = { plugins: null, combined: null }; var combinePlugins = function combinePlugins(plugins) { if (!plugins) { return function (css) { return css; }; } var pluginsToString = JSON.stringify(plugins); if (combinedPluginsCache.plugins === pluginsToString) { return combinedPluginsCache.combined; } if (!Array.isArray(plugins) || plugins.some(function (p) { return !Array.isArray(p) && typeof p !== 'string'; })) { throw new Error('`plugins` must be an array of plugins names (string) or an array `[plugin-name, {options}]`'); } combinedPluginsCache.plugins = pluginsToString; combinedPluginsCache.combined = plugins.map(function (plugin, i) { var options = {}; if (Array.isArray(plugin)) { options = plugin[1] || {}; plugin = plugin[0]; if (Object.prototype.hasOwnProperty.call(options, 'babel')) { throw new Error("\n Error while trying to register the styled-jsx plugin: " + plugin + "\n The option name `babel` is reserved.\n "); } } log('Loading plugin from path: ' + plugin); var p = require(plugin); if (p["default"]) { p = p["default"]; } var type = typeof p; if (type !== 'function') { throw new Error("Expected plugin " + plugins[i] + " to be a function but instead got " + type); } return { plugin: p, options: options }; }).reduce(function (previous, _ref3) { var plugin = _ref3.plugin, options = _ref3.options; return function (css, babelOptions) { return plugin(previous ? previous(css, babelOptions) : css, _extends({}, options, { babel: babelOptions })); }; }, null); return combinedPluginsCache.combined; }; exports.combinePlugins = combinePlugins; var getPrefix = function getPrefix(isDynamic, id) { return isDynamic ? '.__jsx-style-dynamic-selector' : "." + id; }; var processCss = function processCss(stylesInfo, options) { var hash = stylesInfo.hash, css = stylesInfo.css, expressions = stylesInfo.expressions, dynamic = stylesInfo.dynamic, location = stylesInfo.location, file = stylesInfo.file, isGlobal = stylesInfo.isGlobal, plugins = stylesInfo.plugins, vendorPrefixes = stylesInfo.vendorPrefixes, sourceMaps = stylesInfo.sourceMaps; var fileInfo = { code: file.code, sourceRoot: file.opts.sourceRoot, filename: file.opts.filename || file.filename }; fileInfo.sourceFileName = file.opts.sourceFileName || file.sourceFileName || // According to https://babeljs.io/docs/en/options#source-map-options // filenameRelative = path.relative(file.opts.cwd, file.opts.filename) // sourceFileName = path.basename(filenameRelative) // or simply // sourceFileName = path.basename(file.opts.filename) fileInfo.filename && _path["default"].basename(fileInfo.filename); var staticClassName = stylesInfo.staticClassName || "jsx-" + hashString(hash); var splitRules = options.splitRules; var useSourceMaps = Boolean(sourceMaps) && !splitRules; var pluginsOptions = { location: { start: _extends({}, location.start), end: _extends({}, location.end) }, vendorPrefixes: vendorPrefixes, sourceMaps: useSourceMaps, isGlobal: isGlobal, filename: fileInfo.filename }; var transformedCss; if (useSourceMaps) { var generator = makeSourceMapGenerator(fileInfo); var filename = fileInfo.sourceFileName; transformedCss = addSourceMaps((0, _styleTransform["default"])(isGlobal ? '' : getPrefix(dynamic, staticClassName), plugins(css, pluginsOptions), { generator: generator, offset: location.start, filename: filename, splitRules: splitRules, vendorPrefixes: vendorPrefixes }), generator, filename); } else { transformedCss = (0, _styleTransform["default"])(isGlobal ? '' : getPrefix(dynamic, staticClassName), plugins(css, pluginsOptions), { splitRules: splitRules, vendorPrefixes: vendorPrefixes }); } if (expressions.length > 0) { if (typeof transformedCss === 'string') { transformedCss = templateLiteralFromPreprocessedCss(transformedCss, expressions); } else { transformedCss = transformedCss.map(function (transformedCss) { return templateLiteralFromPreprocessedCss(transformedCss, expressions); }); } } else if (Array.isArray(transformedCss)) { transformedCss = transformedCss.map(function (transformedCss) { return t.stringLiteral(transformedCss); }); } return { hash: dynamic ? hashString(hash + staticClassName) : hashString(hash), css: transformedCss, expressions: dynamic && expressions }; }; exports.processCss = processCss; var booleanOption = function booleanOption(opts) { var ret; opts.some(function (opt) { if (typeof opt === 'boolean') { ret = opt; return true; } return false; }); return ret; }; exports.booleanOption = booleanOption; var createReactComponentImportDeclaration = function createReactComponentImportDeclaration(state) { var styleModule = typeof state.opts.styleModule === 'string' ? state.opts.styleModule : 'styled-jsx/style'; return t.importDeclaration([t.importDefaultSpecifier(t.identifier(_constants.STYLE_COMPONENT))], t.stringLiteral(styleModule)); }; exports.createReactComponentImportDeclaration = createReactComponentImportDeclaration; var setStateOptions = function setStateOptions(state) { var vendorPrefixes = booleanOption([state.opts.vendorPrefixes, state.file.opts.vendorPrefixes]); state.opts.vendorPrefixes = typeof vendorPrefixes === 'boolean' ? vendorPrefixes : true; var sourceMaps = booleanOption([state.opts.sourceMaps, state.file.opts.sourceMaps]); state.opts.sourceMaps = Boolean(sourceMaps); if (!state.plugins) { state.plugins = combinePlugins(state.opts.plugins, { sourceMaps: state.opts.sourceMaps, vendorPrefixes: state.opts.vendorPrefixes }); } }; exports.setStateOptions = setStateOptions; function log(message) { console.log('[styled-jsx] ' + message); }