From fefd5554826bf38b974cce0424ec522d3e222a3c Mon Sep 17 00:00:00 2001 From: Vladimir Kharlampidi Date: Fri, 31 Aug 2018 14:32:50 +0300 Subject: [PATCH] TS: Collec props in object spread Ref #11 --- lib/compiler-generator/generator.js | 4 +- .../process-compilation.js | 2 +- .../find-object-spread-properties.js | 76 +++++++++++++++++ .../react/typescript-generator/index.js | 81 ++++++++++++------- test-component/src/component.jsx | 17 +++- test-component/src/mixins.js | 12 +++ 6 files changed, 158 insertions(+), 34 deletions(-) create mode 100644 lib/compiler-utils/find-object-spread-properties.js create mode 100644 test-component/src/mixins.js diff --git a/lib/compiler-generator/generator.js b/lib/compiler-generator/generator.js index c1b0bb9..16fa58d 100644 --- a/lib/compiler-generator/generator.js +++ b/lib/compiler-generator/generator.js @@ -13,7 +13,7 @@ const porcessReplaceComponentNode = require('./process-replace-component-node'); const processExports = require('./process-exports'); function generator(jsxTransformer, componentTransformer, typescriptGenerator) { - function generate(componentString, config, output) { + function generate(componentString, config, input, output) { const state = new CompilerState(config, output); let modifiedComponentString = componentString; @@ -36,7 +36,7 @@ function generator(jsxTransformer, componentTransformer, typescriptGenerator) { const { helpers: jsxHelpers } = jsxTransformer({ ast, name, functional, componentNode, state, config }); componentTransformer({ ast, name, functional, componentNode, state, config, jsxHelpers }); if (config.typeScriptDefinitions && typescriptGenerator) { - typescriptGenerator({ ast, name, functional, componentNode, state, config }); + typescriptGenerator({ ast, name, functional, componentNode, state, config, input }); } processDeclarations(ast, state.declarations); diff --git a/lib/compiler-io/compilation-processing/process-compilation.js b/lib/compiler-io/compilation-processing/process-compilation.js index d3531d6..4cfedd5 100644 --- a/lib/compiler-io/compilation-processing/process-compilation.js +++ b/lib/compiler-io/compilation-processing/process-compilation.js @@ -13,7 +13,7 @@ const compileAllFiles = (compilerName, filesToProcess, config, compiler) => { basePath: outputBase, filePath: outputPath, }; - const compilerOutput = compiler(file.contents, config, output); + const compilerOutput = compiler(file.contents, config, file, output); if (compilerOutput.transformed && path.extname(output.filePath) === '.jsx') { outputPath = `${outputPath.substr(0, outputPath.lastIndexOf('.'))}.js`; diff --git a/lib/compiler-utils/find-object-spread-properties.js b/lib/compiler-utils/find-object-spread-properties.js new file mode 100644 index 0000000..8f32c0c --- /dev/null +++ b/lib/compiler-utils/find-object-spread-properties.js @@ -0,0 +1,76 @@ +const path = require('path'); +const fs = require('fs'); +const walk = require('./walk'); +const codeToAst = require('./code-to-ast'); + +function searchPropsInVariableDeclaration(ast, declarationName) { + let foundProps; + walk(ast, { + VariableDeclaration(node) { + node.declarations.forEach((declaration) => { + if (declaration.id && declaration.id.name === declarationName && declaration.init && declaration.init.properties) { + foundProps = declaration.init.properties; + } + }); + }, + }); + return foundProps; +} + +function findObjectSpreadProperties(ast, declarationName, filePath) { + let objProps; + const objName = declarationName.split('.')[0]; + const objPath = declarationName.split('.').filter((el, index) => index > 0); + + let importPath; + let isExternal; + let importDefault; + ast.body.forEach((node) => { + if (node.type === 'ImportDeclaration') { + node.specifiers.forEach((specifier) => { + if (specifier.local && specifier.local.name === objName) { + importPath = path.join(path.dirname(filePath), node.source.value); + if (importPath.indexOf('.js') < 0) importPath += '.js'; + isExternal = true; + if (specifier.type === 'ImportDefaultSpecifier') importDefault = true; + } + }); + } + }); + + if (isExternal && importPath) { + const depFileContent = fs.readFileSync(importPath, 'utf8'); + const depFileAst = codeToAst(depFileContent); + + if (importDefault) { + walk(depFileAst, { + ExportDefaultDeclaration(node) { + if (node.declaration.type === 'ObjectExpression') { + objProps = node.declaration.properties; + } else if (node.declaration.type === 'Identifier') { + objProps = searchPropsInVariableDeclaration(depFileAst, node.declaration.name); + } + }, + }); + } else { + objProps = searchPropsInVariableDeclaration(depFileAst, objName); + } + } + + if (!isExternal) { + objProps = searchPropsInVariableDeclaration(ast, objName); + } + + if (objProps && objPath) { + objPath.forEach((pathPart) => { + objProps.forEach((objProp) => { + if (objProp.key && objProp.key.name === pathPart && objProp.value.type === 'ObjectExpression') { + objProps = objProp.value.properties; + } + }); + }); + } + return objProps; +} + +module.exports = findObjectSpreadProperties; diff --git a/lib/compilers/react/typescript-generator/index.js b/lib/compilers/react/typescript-generator/index.js index 43da72d..0d57b67 100644 --- a/lib/compilers/react/typescript-generator/index.js +++ b/lib/compilers/react/typescript-generator/index.js @@ -1,6 +1,7 @@ const toCamelCase = require('../../../compiler-utils/to-camel-case'); const astToCode = require('../../../compiler-utils/ast-to-code'); const walk = require('../../../compiler-utils/walk'); +const findObjectSpreadProperties = require('../../../compiler-utils/find-object-spread-properties'); const traversePhenomeComponent = require('../../../compiler-utils/traverse-phenome-component'); const dtsTemplate = ` @@ -32,7 +33,7 @@ function typeMap(type) { return 'any'; } -function collectProps(componentNode) { +function collectProps(componentNode, ast, input) { const props = [{ name: 'slot', type: 'String', @@ -44,39 +45,59 @@ function collectProps(componentNode) { if (node.type === 'MemberExpression') return astToCode(node, { format: { compact: true } }); return 'any'; } + + function getProp(propNode) { + const prop = { + name: propNode.key.name, + }; + if (propNode.value.type === 'Identifier' || propNode.value.type === 'MemberExpression') { + /* foo: String */ + prop.type = propNode.value.name; + prop.type = getPropType(propNode.value); + prop.required = false; + } else if (propNode.value.type === 'ArrayExpression') { + /* foo: [String, Number] */ + prop.type = propNode.value.elements.map(el => el.name); + prop.type = getPropType(propNode.value); + prop.required = false; + } else if (propNode.value.type === 'ObjectExpression') { + /* foo: { type: String, default: 'bar', required: true } */ + propNode.value.properties.forEach((subProp) => { + if (subProp.key.name === 'type') { + prop.type = getPropType(subProp.value); + } + if (subProp.key.name === 'default') { + prop.default = astToCode(subProp.value, { format: { compact: true } }); + } + if (subProp.key.name === 'required') { + prop.required = subProp.value.value; + } + }); + } + return prop; + } traversePhenomeComponent(componentNode, { props(node) { node.value.properties.forEach((propNode) => { - if (propNode.type !== 'Property') return; - const prop = { - name: propNode.key.name, - }; - if (propNode.value.type === 'Identifier' || propNode.value.type === 'MemberExpression') { - /* foo: String */ - prop.type = propNode.value.name; - prop.type = getPropType(propNode.value); - prop.required = false; - } else if (propNode.value.type === 'ArrayExpression') { - /* foo: [String, Number] */ - prop.type = propNode.value.elements.map(el => el.name); - prop.type = getPropType(propNode.value); - prop.required = false; - } else if (propNode.value.type === 'ObjectExpression') { - /* foo: { type: String, default: 'bar', required: true } */ - propNode.value.properties.forEach((subProp) => { - if (subProp.key.name === 'type') { - prop.type = getPropType(subProp.value); - } - if (subProp.key.name === 'default') { - prop.default = astToCode(subProp.value, { format: { compact: true } }); + if (propNode.type !== 'Property') { + if (propNode.type === 'SpreadElement') { + let objProps; + if (propNode.argument.type === 'Identifier') { + objProps = findObjectSpreadProperties(ast, propNode.argument.name, input.fullPath); + } else if (propNode.argument.type === 'MemberExpression') { + objProps = findObjectSpreadProperties(ast, astToCode(propNode.argument), input.fullPath); } - if (subProp.key.name === 'required') { - prop.required = subProp.value.value; + if (objProps) { + objProps.forEach((objPropNode) => { + const prop = getProp(objPropNode); + if (prop) props.push(prop); + }); } - }); + } + return; } - - props.push(prop); + const prop = getProp(propNode); + if (prop) props.push(prop); }); }, }); @@ -184,9 +205,9 @@ function renderMethods(methods, indent) { .join(`\n${indent}`); } -const generate = ({ name = 'MyComponent', componentNode, state }) => { +const generate = ({ name = 'MyComponent', componentNode, state, ast, input }) => { const camelCaseName = toCamelCase(name); - const props = collectProps(componentNode); + const props = collectProps(componentNode, ast, input); const events = collectEvents(componentNode); const methods = collectMethods(componentNode); const renderedProps = renderProps(props, ' '); diff --git a/test-component/src/component.jsx b/test-component/src/component.jsx index ec9b001..9ec7e6a 100644 --- a/test-component/src/component.jsx +++ b/test-component/src/component.jsx @@ -1,3 +1,16 @@ +import Mixins from './mixins'; + +const moreProps = { + moreFoo: String, +}; +const deepProps = { + moreDeepProps: { + eventMoreDeepProps: { + superDeepFoo: String, + }, + } +} + export default { name: 'my-phenome-component', props: { @@ -10,7 +23,9 @@ export default { required: true, }, two: window.FormData, - dt: Date, + date: Date, + ...moreProps, + ...deepProps.moreDeepProps.eventMoreDeepProps, ...Mixins.colorProps, }, methods: { diff --git a/test-component/src/mixins.js b/test-component/src/mixins.js new file mode 100644 index 0000000..2b8f929 --- /dev/null +++ b/test-component/src/mixins.js @@ -0,0 +1,12 @@ +const Mixins = { + colorProps: { + color: String, + colorTheme: String, + textColor: String, + bgColor: String, + borderColor: String, + rippleColor: String, + themeDark: Boolean, + }, +} +export default Mixins; \ No newline at end of file