From e2617a46f57b6953274c44d73f24e4a6df2cc48f Mon Sep 17 00:00:00 2001 From: ChrisCindy Date: Thu, 24 Oct 2019 15:27:00 +0800 Subject: [PATCH] [RELEASE] JSX2MP @ 1024 (#1423) * chore: bump jsx2mp-loader version * feat: generating source maps (#1433) * refactor: generate inline source map in npm file * feat: add source map for rax code in watch mode * fix: add default sourceFileName to avoid test failure * refactor: use inline source map to support alipay IDE * fix: fix lint error * chore: remove useless import * feat: miniapp optimize build mode (#1434) * feat: clean dist dir before generating * feat: uglify runtime in build mode * fix: lint error * perf: minify js/css in build mode * feat: minify Code in build mode and do dce to all rax code * test: update compile result test case * fix: lint error * refactor: extract output logic to a single file * fix: copy path * fix: path resolve error in usingComponents * fix: path error in script when using node modules * fix: child component render * fix: child component render * chore: remove useless code * feat: support logical expression with jsx (#1424) * feat: support logical expression with jsx * fix: nested logical expression * chore: optimize warning * feat: support useRef * feat: add disable copy npm mode * chore: remove useless code * chore: add refs.length is 0 condition * feat: support ref is string * fix: fragment missing * refactor: extract transformCode to output function * fix: lint error * test: update test case of generating correct js code * feat: support build differentiated platform (#1442) * chore: opitimize code * feat: disable copy npm (#1441) * feat: add disable copy npm mode * refactor: extract transformCode to output function * fix: lint error * test: update test case of generating correct js code * chore: opitimize code * chore: bump version --- packages/jsx-compiler/package.json | 3 +- packages/jsx-compiler/src/codegen/genCode.js | 5 +- packages/jsx-compiler/src/codegen/index.js | 2 +- .../src/modules/__tests__/attribute.js | 27 ++++ .../src/modules/__tests__/condition.js | 28 +++- .../src/modules/__tests__/element.js | 9 -- .../jsx-compiler/src/modules/attribute.js | 44 +++++++ packages/jsx-compiler/src/modules/code.js | 106 +++++++++++---- .../jsx-compiler/src/modules/components.js | 18 ++- .../jsx-compiler/src/modules/condition.js | 111 ++++++++++++---- packages/jsx-compiler/src/modules/element.js | 18 --- packages/jsx-compiler/src/modules/index.js | 2 + packages/jsx-compiler/src/modules/jsx-plus.js | 1 + packages/jsx2mp-cli/__tests__/component.js | 16 +-- packages/jsx2mp-cli/bin/jsx2mp-build.js | 6 +- packages/jsx2mp-cli/bin/jsx2mp-start.js | 7 +- packages/jsx2mp-cli/getWebpackConfig.js | 25 ++-- packages/jsx2mp-cli/index.js | 18 ++- packages/jsx2mp-cli/package.json | 4 +- packages/jsx2mp-cli/plugins/runtime.js | 13 +- packages/jsx2mp-loader/package.json | 9 +- packages/jsx2mp-loader/src/app-loader.js | 47 +++++-- .../src/babel-plugin-rename-import.js | 55 ++++---- .../jsx2mp-loader/src/component-loader.js | 62 +++++---- packages/jsx2mp-loader/src/output.js | 121 ++++++++++++++++++ packages/jsx2mp-loader/src/page-loader.js | 59 +++++---- packages/jsx2mp-loader/src/script-loader.js | 113 ++++++---------- .../jsx2mp-loader/src/utils/addSourceMap.js | 11 ++ packages/jsx2mp-loader/src/utils/dce.js | 40 ++++++ .../jsx2mp-loader/src/utils/minifyCode.js | 46 +++++++ packages/jsx2mp-runtime/package.json | 2 +- packages/jsx2mp-runtime/src/bridge.js | 3 +- packages/jsx2mp-runtime/src/component.js | 45 ++++--- packages/jsx2mp-runtime/src/enqueueRender.js | 1 - packages/jsx2mp-runtime/src/hooks.js | 8 +- packages/jsx2mp-runtime/src/updater.js | 26 ++-- 36 files changed, 785 insertions(+), 326 deletions(-) create mode 100644 packages/jsx-compiler/src/modules/__tests__/attribute.js create mode 100644 packages/jsx-compiler/src/modules/attribute.js create mode 100644 packages/jsx2mp-loader/src/output.js create mode 100644 packages/jsx2mp-loader/src/utils/addSourceMap.js create mode 100644 packages/jsx2mp-loader/src/utils/dce.js create mode 100644 packages/jsx2mp-loader/src/utils/minifyCode.js diff --git a/packages/jsx-compiler/package.json b/packages/jsx-compiler/package.json index afdb500b1c..c527699e72 100644 --- a/packages/jsx-compiler/package.json +++ b/packages/jsx-compiler/package.json @@ -1,6 +1,6 @@ { "name": "jsx-compiler", - "version": "0.3.1", + "version": "0.3.2", "license": "BSD-3-Clause", "description": "Parser for Rax JSX Statements.", "main": "src/index.js", @@ -25,6 +25,7 @@ "fs-extra": "^7.0.1", "kebab-case": "^1.0.0", "md5": "^2.2.1", + "resolve": "^1.12.0", "stylesheet-loader": "^0.6.6-0" }, "devDependencies": { diff --git a/packages/jsx-compiler/src/codegen/genCode.js b/packages/jsx-compiler/src/codegen/genCode.js index 75a325ad93..f2841e39c3 100644 --- a/packages/jsx-compiler/src/codegen/genCode.js +++ b/packages/jsx-compiler/src/codegen/genCode.js @@ -1,6 +1,9 @@ const generate = require('@babel/generator').default; -const generateOptions = {}; +const generateOptions = { + sourceFileName: '', + sourceMaps: true +}; /** * Generate code and map from babel ast. diff --git a/packages/jsx-compiler/src/codegen/index.js b/packages/jsx-compiler/src/codegen/index.js index 6d1b8fdb5e..4ad45717e3 100644 --- a/packages/jsx-compiler/src/codegen/index.js +++ b/packages/jsx-compiler/src/codegen/index.js @@ -3,7 +3,7 @@ const genCode = require('./genCode'); const { baseOptions } = require('../options'); function generate(parsed, options = baseOptions) { - const { code, map } = genCode(parsed.ast); + const { code, map } = genCode(parsed.ast, options); const ret = { code, map, // config, template, style and others should be generated in plugin modules. diff --git a/packages/jsx-compiler/src/modules/__tests__/attribute.js b/packages/jsx-compiler/src/modules/__tests__/attribute.js new file mode 100644 index 0000000000..6d7a22a3d1 --- /dev/null +++ b/packages/jsx-compiler/src/modules/__tests__/attribute.js @@ -0,0 +1,27 @@ +const t = require('@babel/types'); +const { _transformAttribute } = require('../attribute'); +const { parseExpression } = require('../../parser'); +const adapter = require('../../adapter').ali; +const genCode = require('../../codegen/genCode'); + +describe('Transform JSX Attribute', () => { + it('should transform attribute name is key', () => { + const code = 'test'; + const ast = parseExpression(code); + _transformAttribute(ast, code, adapter); + expect(genCode(ast).code).toEqual('test'); + }); + it('should transform attribute name is className', () => { + const code = 'test'; + const ast = parseExpression(code); + _transformAttribute(ast, code, adapter); + expect(genCode(ast).code).toEqual('test'); + }); + it("should collect attribute name is ref and parse it's value as a string", () => { + const code = 'test'; + const ast = parseExpression(code); + const refs = _transformAttribute(ast, code, adapter); + expect(genCode(ast).code).toEqual('test'); + expect(refs).toEqual([t.stringLiteral('scrollViewRef')]); + }); +}); diff --git a/packages/jsx-compiler/src/modules/__tests__/condition.js b/packages/jsx-compiler/src/modules/__tests__/condition.js index e3f254003a..01df831fe9 100644 --- a/packages/jsx-compiler/src/modules/__tests__/condition.js +++ b/packages/jsx-compiler/src/modules/__tests__/condition.js @@ -39,7 +39,7 @@ describe('Transform condition', () => { const dynamicValue = _transformTemplate(ast, adapter, {}); expect(genCode(ast).code).toEqual(''); - expect(genDynamicValue(dynamicValue)).toEqual('{ foo: foo, bar: bar }'); + expect(genDynamicValue(dynamicValue)).toEqual('{ bar: bar, foo: foo }'); }); it("transform condition's alternate is conditional expression", () => { @@ -49,7 +49,7 @@ describe('Transform condition', () => { const dynamicValue = _transformTemplate(ast, adapter, {}); expect(genCode(ast).code).toEqual('xxx'); - expect(genDynamicValue(dynamicValue)).toEqual('{ empty: empty, loading: loading }'); + expect(genDynamicValue(dynamicValue)).toEqual('{ loading: loading, empty: empty }'); }); it('skip list dynamic value', () => { @@ -88,6 +88,30 @@ describe('Transform condition', () => { `); expect(genDynamicValue(dynamicValue)).toEqual('{ tabList: tabList }'); }); + + it('transform simple logical expression', () => { + const ast = parseExpression(` + + { a && 1} + + `); + _transformTemplate(ast, adapter, {}); + expect(genCode(ast).code).toEqual(` + 1{a} + `); + }); + + it('transform nested logical expression', () => { + const ast = parseExpression(` + + { a || b && 1} + + `); + _transformTemplate(ast, adapter, {}); + expect(genCode(ast).code).toEqual(` + 1{b}{a} + `); + }); }); describe('Transiform condition render function', () => { diff --git a/packages/jsx-compiler/src/modules/__tests__/element.js b/packages/jsx-compiler/src/modules/__tests__/element.js index d281529499..2634ebd1d7 100644 --- a/packages/jsx-compiler/src/modules/__tests__/element.js +++ b/packages/jsx-compiler/src/modules/__tests__/element.js @@ -109,15 +109,6 @@ describe('Transform JSXElement', () => { expect(genInlineCode(ast).code).toEqual('{{ _d0 ? _d0.b[_d1.d] : 1 }}'); expect(genDynamicAttrs(dynamicValues)).toEqual('{ _d0: a, _d1: c }'); }); - - it('should adapt attribute key', () => { - const sourceCode = '{ bar }'; - const ast = parseExpression(sourceCode); - const { dynamicValues } = _transform(ast, null, adapter, sourceCode); - const code = genInlineCode(ast).code; - expect(code).toEqual('{{ _d0 }}'); - expect(genDynamicAttrs(dynamicValues)).toEqual('{ _d0: bar }'); - }); }); describe('event handlers', () => { diff --git a/packages/jsx-compiler/src/modules/attribute.js b/packages/jsx-compiler/src/modules/attribute.js new file mode 100644 index 0000000000..9020d65b60 --- /dev/null +++ b/packages/jsx-compiler/src/modules/attribute.js @@ -0,0 +1,44 @@ +const t = require('@babel/types'); +const traverse = require('../utils/traverseNodePath'); +const genExpression = require('../codegen/genExpression'); +const CodeError = require('../utils/CodeError'); + +function transformAttribute(ast, code, adapter) { + const refs = []; + traverse(ast, { + JSXAttribute(path) { + const { node } = path; + const attrName = node.name.name; + switch (attrName) { + case 'key': + node.name.name = adapter.key; + break; + case 'className': + node.name.name = adapter.className; + break; + case 'ref': + if (t.isJSXExpressionContainer(node.value)) { + node.value = t.stringLiteral(genExpression(node.value.expression)); + } + if (t.isStringLiteral(node.value)) { + refs.push(node.value); + } else { + throw new CodeError(code, node, path.loc, "Ref's type must be string or jsxExpressionContainer"); + } + break; + default: + path.stop(); + } + } + }); + return refs; +} + +module.exports = { + parse(parsed, code, options) { + parsed.refs = transformAttribute(parsed.templateAST, code, options.adapter); + }, + + // For test cases. + _transformAttribute: transformAttribute, +}; diff --git a/packages/jsx-compiler/src/modules/code.js b/packages/jsx-compiler/src/modules/code.js index bccc231722..15e259dd4d 100644 --- a/packages/jsx-compiler/src/modules/code.js +++ b/packages/jsx-compiler/src/modules/code.js @@ -20,17 +20,16 @@ const SAFE_CREATE_STYLE = '__create_style__'; const USE_EFFECT = 'useEffect'; const USE_STATE = 'useState'; const USE_CONTEXT = 'useContext'; -const USE_HISTORY = 'useHistory'; -const USE_LOCATION = 'useLocation'; +const USE_REF = 'useRef'; const EXPORTED_DEF = '__def__'; -const RUNTIME = '/npm/jsx2mp-runtime'; +const RUNTIME = 'jsx2mp-runtime'; +const getRuntimeByPlatform = (platform) => `${RUNTIME}/dist/jsx2mp-runtime.${platform}.esm`; const isAppRuntime = (mod) => mod === 'rax-app'; const isFileModule = (mod) => /\.(png|jpe?g|gif|bmp|webp)$/.test(mod); -const isHooksAPI = (node) => [USE_EFFECT, USE_STATE, USE_CONTEXT, - USE_HISTORY, USE_LOCATION].includes(node.name); +const isCoreHooksAPI = (node) => [USE_EFFECT, USE_STATE, USE_CONTEXT, USE_REF].includes(node.name); function getConstructor(type) { switch (type) { @@ -50,13 +49,14 @@ function getConstructor(type) { module.exports = { parse(parsed, code, options) { const { defaultExportedPath, eventHandlers = [] } = parsed; - if (options.type !== 'app' && (!defaultExportedPath || !defaultExportedPath.node)) { + const { platform, type, cwd, outputPath, sourcePath, resourcePath, disableCopyNpm } = options; + if (type !== 'app' && (!defaultExportedPath || !defaultExportedPath.node)) { // Can not found default export, otherwise app.js is excluded. return; } let userDefineType; - if (options.type === 'app') { + if (type === 'app') { userDefineType = 'function'; } else if (isFunctionComponent(defaultExportedPath)) { // replace with class def. userDefineType = 'function'; @@ -91,20 +91,22 @@ module.exports = { } const hooks = collectHooks(parsed.renderFunctionPath); - - const targetFileDir = dirname(join(options.outputPath, relative(options.sourcePath, options.resourcePath))); + const targetFileDir = dirname(join(outputPath, relative(sourcePath, resourcePath))); + const runtimePath = getRuntimePath(outputPath, targetFileDir, platform, disableCopyNpm); removeRaxImports(parsed.ast); - renameCoreModule(parsed.ast, options.outputPath, targetFileDir); + renameCoreModule(parsed.ast, runtimePath); renameFileModule(parsed.ast); - renameAppConfig(parsed.ast, options.sourcePath, options.resourcePath); + renameAppConfig(parsed.ast, sourcePath, resourcePath); - const currentNodeModulePath = join(options.sourcePath, 'npm'); - const npmRelativePath = relative(dirname(options.resourcePath), currentNodeModulePath); - renameNpmModules(parsed.ast, npmRelativePath, options.resourcePath, options.cwd); + if (!disableCopyNpm) { + const currentNodeModulePath = join(sourcePath, 'npm'); + const npmRelativePath = relative(dirname(resourcePath), currentNodeModulePath); + renameNpmModules(parsed.ast, npmRelativePath, resourcePath, cwd); + } - if (options.type !== 'app') { - addDefine(parsed.ast, options.type, options.outputPath, targetFileDir, userDefineType, eventHandlers, parsed.useCreateStyle, hooks); + if (type !== 'app') { + addDefine(parsed.ast, type, userDefineType, eventHandlers, parsed.useCreateStyle, hooks, runtimePath); } removeDefaultImports(parsed.ast); @@ -112,7 +114,7 @@ module.exports = { /** * updateChildProps: collect props dependencies. */ - if (options.type !== 'app' && parsed.renderFunctionPath) { + if (type !== 'app' && parsed.renderFunctionPath) { const fnBody = parsed.renderFunctionPath.node.body.body; let firstReturnStatementIdx = -1; for (let i = 0, l = fnBody.length; i < l; i++) { @@ -159,6 +161,7 @@ module.exports = { addUpdateData(parsed.dynamicValue, parsed.renderItemFunctions, parsed.renderFunctionPath); addUpdateEvent(parsed.dynamicEvents, parsed.eventHandler, parsed.renderFunctionPath); addProviderIniter(parsed.contextList, parsed.renderFunctionPath); + addRegisterRefs(parsed.refs, parsed.renderFunctionPath); } }, }; @@ -176,14 +179,21 @@ function genTagIdExp(expressions) { return parseExpression(ret); } -function renameCoreModule(ast, outputPath, targetFileDir) { +function getRuntimePath(outputPath, targetFileDir, platform, disableCopyNpm) { + let runtimePath = getRuntimeByPlatform(platform.type); + if (!disableCopyNpm) { + runtimePath = relative(targetFileDir, join(outputPath, 'npm', RUNTIME)); + runtimePath = runtimePath[0] !== '.' ? './' + runtimePath : runtimePath; + } + return runtimePath; +} + +function renameCoreModule(ast, runtimePath) { traverse(ast, { ImportDeclaration(path) { const source = path.get('source'); if (source.isStringLiteral() && isAppRuntime(source.node.value)) { - let runtimeRelativePath = relative(targetFileDir, join(outputPath, RUNTIME)); - runtimeRelativePath = runtimeRelativePath[0] !== '.' ? './' + runtimeRelativePath : runtimeRelativePath; - source.replaceWith(t.stringLiteral(runtimeRelativePath)); + source.replaceWith(t.stringLiteral(runtimePath)); } } }); @@ -282,7 +292,7 @@ function renameNpmModules(ast, npmRelativePath, filename, cwd) { }); } -function addDefine(ast, type, outputPath, targetFileDir, userDefineType, eventHandlers, useCreateStyle, hooks) { +function addDefine(ast, type, userDefineType, eventHandlers, useCreateStyle, hooks, runtimePath) { let safeCreateInstanceId; let importedIdentifier; switch (type) { @@ -323,13 +333,10 @@ function addDefine(ast, type, outputPath, targetFileDir, userDefineType, eventHa )); } - let runtimeRelativePath = relative(targetFileDir, join(outputPath, RUNTIME)); - runtimeRelativePath = runtimeRelativePath[0] !== '.' ? './' + runtimeRelativePath : runtimeRelativePath; - path.node.body.unshift( t.importDeclaration( specifiers, - t.stringLiteral(runtimeRelativePath) + t.stringLiteral(runtimePath) ) ); @@ -405,7 +412,7 @@ function collectHooks(root) { traverse(root, { CallExpression(path) { const { node } = path; - if (t.isIdentifier(node.callee) && isHooksAPI(node.callee)) { + if (t.isIdentifier(node.callee) && isCoreHooksAPI(node.callee)) { ret[node.callee.name] = true; } } @@ -468,6 +475,51 @@ function addProviderIniter(contextList, renderFunctionPath) { } } +/** + * Insert register ref method + * @param {Array} refs + * @param {Object} renderFunctionPath + * */ +function addRegisterRefs(refs, renderFunctionPath) { + const registerRefsMethods = t.memberExpression( + t.thisExpression(), + t.identifier('_registerRefs') + ); + const fnBody = renderFunctionPath.node.body.body; + /** + * this._registerRefs([ + * { + * name: 'scrollViewRef', + * method: scrollViewRef + * } + * ]) + * */ + const scopedRefs = []; + const stringRefs = []; + refs.map(ref => { + if (renderFunctionPath.scope.hasBinding(ref.value)) { + scopedRefs.push(ref); + } else { + stringRefs.push(ref); + } + }); + if (scopedRefs.length > 0) { + fnBody.push(t.expressionStatement(t.callExpression(registerRefsMethods, [ + t.arrayExpression(scopedRefs.map(ref => { + return t.objectExpression([t.objectProperty(t.stringLiteral('name'), ref), + t.objectProperty(t.stringLiteral('method'), t.identifier(ref.value))]); + })) + ]))); + } + if (stringRefs.length > 0) { + fnBody.unshift(t.expressionStatement(t.callExpression(registerRefsMethods, [ + t.arrayExpression(stringRefs.map(ref => { + return t.objectExpression([t.objectProperty(t.stringLiteral('name'), ref)]); + })) + ]))); + } +} + /** * For that alipay build folder can not contain `@`, escape to `_`. */ diff --git a/packages/jsx-compiler/src/modules/components.js b/packages/jsx-compiler/src/modules/components.js index b0c06b52d4..f722977ed6 100644 --- a/packages/jsx-compiler/src/modules/components.js +++ b/packages/jsx-compiler/src/modules/components.js @@ -1,5 +1,6 @@ -const { join, relative, dirname } = require('path'); +const { join, relative, dirname, resolve } = require('path'); const { readJSONSync } = require('fs-extra'); +const resolveModule = require('resolve'); const t = require('@babel/types'); const { _transform: transformTemplate } = require('./element'); const genExpression = require('../codegen/genExpression'); @@ -278,7 +279,7 @@ function getComponentConfig(pkgName, resourcePath) { // for tnpm, the package name will be like _rax-image@1.1.2@rax-image function getRealNpmPkgName(filePath) { const result = PKG_NAME_REG.exec(filePath); - return result && result[1].replace(/@/g, '_'); + return result && result[1]; } function getComponentPath(alias, options) { @@ -293,15 +294,24 @@ function getComponentPath(alias, options) { moduleResolve(options.resourcePath, alias.from, '.js'); return filename; } else { - const realNpmFile = require.resolve(alias.from, { paths: [options.resourcePath] }); + const { disableCopyNpm } = options; + const realNpmFile = resolveModule.sync(alias.from, { basedir: dirname(options.resourcePath), preserveSymlinks: false }); const pkgName = getRealNpmPkgName(realNpmFile); // npm module const pkg = getComponentConfig(alias.from, options.resourcePath); if (pkg.miniappConfig && pkg.miniappConfig.main) { + if (disableCopyNpm) { + return join(pkg.name, pkg.miniappConfig.main); + } + const targetFileDir = dirname(join(options.outputPath, relative(options.sourcePath, options.resourcePath))); let npmRelativePath = relative(targetFileDir, join(options.outputPath, '/npm')); npmRelativePath = npmRelativePath[0] !== '.' ? './' + npmRelativePath : npmRelativePath; - return './' + join(npmRelativePath, pkgName, pkg.miniappConfig.main); + + const miniappConfigRelativePath = relative(pkg.main, pkg.miniappConfig.main); + const realMiniappAbsPath = resolve(realNpmFile, miniappConfigRelativePath); + const realMiniappRelativePath = realMiniappAbsPath.slice(realMiniappAbsPath.indexOf(pkgName) + pkgName.length); + return './' + join(npmRelativePath, pkgName.replace(/@/g, '_'), realMiniappRelativePath); } else { console.warn( 'Can not found compatible rax miniapp component "' + pkg.name + '".', diff --git a/packages/jsx-compiler/src/modules/condition.js b/packages/jsx-compiler/src/modules/condition.js index 15000cdda2..46f3d9554b 100644 --- a/packages/jsx-compiler/src/modules/condition.js +++ b/packages/jsx-compiler/src/modules/condition.js @@ -3,6 +3,8 @@ const traverse = require('../utils/traverseNodePath'); const createJSX = require('../utils/createJSX'); const createBinding = require('../utils/createBinding'); const genExpression = require('../codegen/genExpression'); +const CodeError = require('../utils/CodeError'); +const chalk = require('chalk'); const TEMPLATE_AST = 'templateAST'; const RENDER_FN_PATH = 'renderFunctionPath'; @@ -57,7 +59,8 @@ function transformRenderFunction(ast, adapter) { consequent.body.map(({ expression }) => { if ( t.isAssignmentExpression(expression) && - expression.operator === '=' + expression.operator === '=' && + t.isIdentifier(expression.left) ) { let shouldRemove = false; const varName = expression.left.name; @@ -66,10 +69,6 @@ function transformRenderFunction(ast, adapter) { 'block', ); } - /** - * Todo: - * 1. value tranmits - */ let testAttrName = adapter.if; const parentPathAlternate = nodePath.parent.alternate; /** @@ -110,7 +109,8 @@ function transformRenderFunction(ast, adapter) { alternate.body.map(({ expression }) => { if ( t.isAssignmentExpression(expression) && - expression.operator === '=' + expression.operator === '=' && + t.isIdentifier(expression.left) ) { const varName = expression.left.name; const rightNode = expression.right; @@ -138,7 +138,7 @@ function transformRenderFunction(ast, adapter) { return templateVariables; } -function transformTemplate(ast, adapter, templateVariables) { +function transformTemplate(ast, adapter, templateVariables, code) { const dynamicValue = {}; traverse(ast, { @@ -151,7 +151,7 @@ function transformTemplate(ast, adapter, templateVariables) { switch (node.expression.type) { case 'ConditionalExpression': { - const { replacement } = transformConditionalExpression(path, path.node.expression, adapter, dynamicValue); + const { replacement } = transformConditionalExpression(path, node.expression, { adapter, dynamicValue, code }); path.replaceWithMultiple(replacement); break; } @@ -171,24 +171,65 @@ function transformTemplate(ast, adapter, templateVariables) { } } }, + LogicalExpression(path) { + if (path.parentPath.isJSXExpressionContainer()) { + const { right, left, operator } = path.node; + let replacement = []; + if (isJSX(left)) { + if (operator === '&&') { + replacement.push(right); + } else if (operator === '||') { + replacement.push(left); + } else { + throw new CodeError(code, path.node, path.node.loc, 'Logical operator only support && or ||'); + } + } else { + let test; + if (operator === '||') { + test = t.unaryExpression('!', left); + } else if (operator === '&&') { + test = left; + } else { + throw new CodeError(code, path.node, path.node.loc, 'Logical operator only support && or ||'); + } + const children = []; + if (/Expression$/.test(right.type)) { + children.push(t.jsxExpressionContainer(right)); + } else { + children.push(right); + } + replacement.push(createJSX('block', { + [adapter.if]: generateConditionValue(test, {adapter, dynamicValue}) + }, children)); + replacement.push(createJSX('block', { + [adapter.else]: null, + }, [t.jsxExpressionContainer(left)])); + if (/Expression$/.test(left.type)) { + console.log(chalk.yellow("When logicalExpression's left node is an expression, please write JSX directly instead of using a variable which is assigned a JSX element." + )); + } + } + path.parentPath.replaceWithMultiple(replacement); + } else { + path.skip(); + } + } }); return dynamicValue; } -function transformConditionalExpression(path, expression, adapter, dynamicValue) { +/** + * @param {Object} path + * jsxExpressionContainer + * @param {Object} expression + * Condition Expression + * @param {{ adapter: Object, dynamicValue: Object, code: String }} options + * @returns {{ replacement: Object, dynamicValue: Object }} + * */ +function transformConditionalExpression(path, expression, options) { let { test, consequent, alternate } = expression; - let conditionValue; - if (/Expression$/.test(test.type)) { - conditionValue = t.jsxExpressionContainer(test); - } else if (t.isStringLiteral(test)) { - conditionValue = test; - } else { - // Other literal types or identifier. - conditionValue = t.stringLiteral(createBinding(genExpression(test))); - if (t.isIdentifier(test)) dynamicValue[test.name] = test; - } const replacement = []; let consequentReplacement = []; let alternateReplacement = []; @@ -198,8 +239,7 @@ function transformConditionalExpression(path, expression, adapter, dynamicValue) consequentReplacement = transformConditionalExpression( path, consequent, - adapter, - dynamicValue + options ).replacement; } else if (/Literal$/.test(consequent.type)) { // Transform from literal type to JSXText Node @@ -217,8 +257,7 @@ function transformConditionalExpression(path, expression, adapter, dynamicValue) alternateReplacement = transformConditionalExpression( path, alternate, - adapter, - dynamicValue + options ).replacement; } else if (t.isNullLiteral(alternate)) { // Ignore null @@ -236,7 +275,7 @@ function transformConditionalExpression(path, expression, adapter, dynamicValue) createJSX( 'block', { - [adapter.if]: conditionValue, + [options.adapter.if]: generateConditionValue(test, options), }, consequentReplacement, ), @@ -247,13 +286,13 @@ function transformConditionalExpression(path, expression, adapter, dynamicValue) createJSX( 'block', { - [adapter.else]: null, + [options.adapter.else]: null, }, alternateReplacement, ), ); } - return { replacement, dynamicValue }; + return { replacement, dynamicValue: options.dynamicValue }; } module.exports = { @@ -265,7 +304,7 @@ module.exports = { if (t.isIdentifier(parsed[TEMPLATE_AST]) && parsed[TEMPLATE_AST].name in templateVariables) { parsed[TEMPLATE_AST] = templateVariables[parsed[TEMPLATE_AST].name].value; } else { - const dynamicValue = transformTemplate(parsed[TEMPLATE_AST], options.adapter, templateVariables); + const dynamicValue = transformTemplate(parsed[TEMPLATE_AST], options.adapter, templateVariables, code); Object.assign(parsed.dynamicValue = parsed.dynamicValue || {}, dynamicValue); } }, @@ -274,3 +313,21 @@ module.exports = { _transformRenderFunction: transformRenderFunction, _transformTemplate: transformTemplate, }; + +function generateConditionValue(test, options) { + let conditionValue; + if (/Expression$/.test(test.type)) { + conditionValue = t.jsxExpressionContainer(test); + } else if (t.isStringLiteral(test)) { + conditionValue = test; + } else { + // Other literal types or identifier. + conditionValue = t.stringLiteral(createBinding(genExpression(test))); + if (t.isIdentifier(test)) options.dynamicValue[test.name] = test; + } + return conditionValue; +} + +function isJSX(node) { + return ['JSXElement', 'JSXText', 'JSXFragment'].indexOf(node.type) > -1; +} diff --git a/packages/jsx-compiler/src/modules/element.js b/packages/jsx-compiler/src/modules/element.js index 62cd62e015..03fb6e7980 100644 --- a/packages/jsx-compiler/src/modules/element.js +++ b/packages/jsx-compiler/src/modules/element.js @@ -328,24 +328,6 @@ function transformTemplate(ast, scope = null, adapter, sourceCode, componentDepe } traverse(ast, { - JSXAttribute(path) { - const { node } = path; - const attrName = node.name.name; - // adapt the key attribute - if (attrName === 'key') { - node.name.name = adapter.key; - } - // Remove ref. - if (attrName === 'ref') { - path.remove(); - } - if (attrName === 'className') { - node.name.name = adapter.className; - } - // if (attrName === 'style') { - // node.name.name = adapter.style; - // } - }, JSXExpressionContainer: handleJSXExpressionContainer, JSXOpeningElement: { exit(path) { diff --git a/packages/jsx-compiler/src/modules/index.js b/packages/jsx-compiler/src/modules/index.js index 732b9caff1..31063b65f0 100644 --- a/packages/jsx-compiler/src/modules/index.js +++ b/packages/jsx-compiler/src/modules/index.js @@ -11,6 +11,8 @@ module.exports = [ require('./code'), // Handle template attrs require('./element'), + // Handle jsx attribute + require('./attribute'), // Handle template style attr require('./style'), // Handle Rax base components. diff --git a/packages/jsx-compiler/src/modules/jsx-plus.js b/packages/jsx-compiler/src/modules/jsx-plus.js index 4d979e85b3..61edd0a6cb 100644 --- a/packages/jsx-compiler/src/modules/jsx-plus.js +++ b/packages/jsx-compiler/src/modules/jsx-plus.js @@ -226,6 +226,7 @@ module.exports = { if (parsed.renderFunctionPath) { transformDirectiveCondition(parsed.templateAST, options.adapter); transformDirectiveList(parsed.templateAST, code, options.adapter); + transformComponentFragment(parsed.templateAST); } }, _transformList: transformDirectiveList, diff --git a/packages/jsx2mp-cli/__tests__/component.js b/packages/jsx2mp-cli/__tests__/component.js index 4ef7597282..233ac2a1da 100644 --- a/packages/jsx2mp-cli/__tests__/component.js +++ b/packages/jsx2mp-cli/__tests__/component.js @@ -33,24 +33,12 @@ describe('Component compiled result', () => { expect(axmlContent).toEqual( ` Hello World! - - `); + `); }); it('should return correct js', () => { expect(jsContent).toEqual( - `import { createComponent as __create_component__ } from "./npm/jsx2mp-runtime"; -const img = "./assets/rax.png"; - -const __def__ = function Index() { - this._updateData({ - "_d0": img - }); - - this._updateMethods({}); -}; - -Component(__create_component__(__def__));` + '\"use strict\";var _jsx2mpRuntime=require(\"./npm/jsx2mp-runtime\"),img=\"./assets/rax.png\",__def__=function(){this._updateData({_d0:img}),this._updateMethods({})};Component((0,_jsx2mpRuntime.createComponent)(__def__));' ); }); diff --git a/packages/jsx2mp-cli/bin/jsx2mp-build.js b/packages/jsx2mp-cli/bin/jsx2mp-build.js index 9fa5646955..7eeb3ca8e0 100644 --- a/packages/jsx2mp-cli/bin/jsx2mp-build.js +++ b/packages/jsx2mp-cli/bin/jsx2mp-build.js @@ -9,7 +9,8 @@ program .option('-e, --entry ', 'set entry of component', DEFAULT_ENTRY) .option('-d, --dist ', 'set export path', DEFAULT_DIST) .option('-s, --skip-clear-stdout', 'skip clear stdout of screen', false) - .option('-c --constant-dir ', 'set constant directory to copy', DEFAULT_CONSTANT_DIR) + .option('-c, --constant-dir ', 'set constant directory to copy', DEFAULT_CONSTANT_DIR) + .option('-n, --disable-copy-npm', 'disable copy node_modules action', false) .action((cmd) => { const workDirectory = resolve(process.env.CWD || process.cwd()); const distDirectory = resolve(workDirectory, cmd.dist); @@ -23,7 +24,8 @@ program dist: cmd.dist, platform: cmd.platform, skipClearStdout: cmd.skipClearStdout, - constantDir: cmd.constantDir === '' ? [] : cmd.constantDir.split(',').map(c => c.trim()) + constantDir: cmd.constantDir === '' ? [] : cmd.constantDir.split(',').map(c => c.trim()), + disableCopyNpm: cmd.disableCopyNpm }; require('..').build(options); diff --git a/packages/jsx2mp-cli/bin/jsx2mp-start.js b/packages/jsx2mp-cli/bin/jsx2mp-start.js index a6b3a24ca2..3cd620b1ca 100644 --- a/packages/jsx2mp-cli/bin/jsx2mp-start.js +++ b/packages/jsx2mp-cli/bin/jsx2mp-start.js @@ -9,7 +9,8 @@ program .option('-e, --entry ', 'set entry of component', DEFAULT_ENTRY) .option('-d, --dist ', 'set export path', DEFAULT_DIST) .option('-s, --skip-clear-stdout', 'skip clear stdout of screen', false) - .option('-c --constant-dir ', 'set constant directory to copy', DEFAULT_CONSTANT_DIR) + .option('-c, --constant-dir ', 'set constant directory to copy', DEFAULT_CONSTANT_DIR) + .option('-n, --disable-copy-npm', 'disable copy node_modules action', false) .action((cmd) => { const workDirectory = resolve(process.env.CWD || process.cwd()); const distDirectory = resolve(workDirectory, cmd.dist); @@ -23,9 +24,9 @@ program dist: cmd.dist, platform: cmd.platform, skipClearStdout: cmd.skipClearStdout, - constantDir: cmd.constantDir === '' ? [] : cmd.constantDir.split(',').map(c => c.trim()) + constantDir: cmd.constantDir === '' ? [] : cmd.constantDir.split(',').map(c => c.trim()), + disableCopyNpm: cmd.disableCopyNpm }; - require('..').watch(options); }); diff --git a/packages/jsx2mp-cli/getWebpackConfig.js b/packages/jsx2mp-cli/getWebpackConfig.js index f08c4c98a7..59cac06270 100644 --- a/packages/jsx2mp-cli/getWebpackConfig.js +++ b/packages/jsx2mp-cli/getWebpackConfig.js @@ -33,12 +33,14 @@ function getBabelConfig() { function getEntry(type, cwd, entryFilePath, options) { const entryPath = dirname(entryFilePath); const entry = {}; - const { platform = 'ali', constantDir } = options; + const { platform = 'ali', constantDir, mode, disableCopyNpm } = options; const loaderParams = JSON.stringify({ platform: platformConfig[platform], entryPath: entryFilePath, - constantDir + constantDir, + mode, + disableCopyNpm }); if (type === 'project') { @@ -50,9 +52,11 @@ function getEntry(type, cwd, entryFilePath, options) { console.error('Can not found app.json in current work directory, please check.'); process.exit(1); } - entry.app = AppLoader + '?' + JSON.stringify({ entryPath, platform: platformConfig[platform] }) + '!./' + join(entryPath, 'app.js'); + entry.app = AppLoader + '?' + JSON.stringify({ entryPath, platform: platformConfig[platform], mode, disableCopyNpm }) + '!./' + join(entryPath, 'app.js'); if (Array.isArray(appConfig.routes)) { - appConfig.routes.forEach(({ source, component }) => { + appConfig.routes.filter(({ targets }) => { + return !Array.isArray(targets) || targets.indexOf('miniapp') > -1; + }).forEach(({ source, component }) => { component = source || component; entry['page@' + component] = PageLoader + '?' + loaderParams + '!' + getDepPath(component, entryPath); }); @@ -85,12 +89,12 @@ function getDepPath(path, rootContext) { const cwd = process.cwd(); module.exports = (options = {}) => { - let { entryPath, type, workDirectory, distDirectory, platform = 'ali', mode } = options; + let { entryPath, type, workDirectory, distDirectory, platform = 'ali', mode, disableCopyNpm } = options; if (entryPath[0] !== '.') entryPath = './' + entryPath; entryPath = moduleResolve(workDirectory, entryPath, '.js') || moduleResolve(workDirectory, entryPath, '.jsx') || entryPath; const relativeEntryFilePath = './' + relative(workDirectory, entryPath); // src/app.js or src/mobile/index.js - return { + const config = { mode: 'production', // Will be fast entry: getEntry(type, cwd, relativeEntryFilePath, options), output: { @@ -108,7 +112,8 @@ module.exports = (options = {}) => { options: { mode: options.mode, entryPath: relativeEntryFilePath, - platform: platformConfig[platform] + platform: platformConfig[platform], + disableCopyNpm: options.disableCopyNpm }, }, { @@ -146,7 +151,6 @@ module.exports = (options = {}) => { NODE_ENV: mode === 'build' ? '"production"' : '"development"', } }), - new RuntimeWebpackPlugin({ platform }), new webpack.ProgressPlugin( (percentage, message) => { if (percentage === 0) { buildStartTime = Date.now(); @@ -158,4 +162,9 @@ module.exports = (options = {}) => { }) ], }; + + if (!disableCopyNpm) { + config.plugins.push(new RuntimeWebpackPlugin({ platform, mode })); + } + return config; }; diff --git a/packages/jsx2mp-cli/index.js b/packages/jsx2mp-cli/index.js index 629243423f..e5e4ad0f3a 100644 --- a/packages/jsx2mp-cli/index.js +++ b/packages/jsx2mp-cli/index.js @@ -4,6 +4,7 @@ const mergeWebpack = require('webpack-merge'); const webpack = require('webpack'); const consoleClear = require('console-clear'); const chalk = require('chalk'); +const del = require('del'); const getWebpackConfig = require('./getWebpackConfig'); const spinner = require('./utils/spinner'); const { getCurrentDirectoryPath } = require('./utils/file'); @@ -26,9 +27,15 @@ function build(options = {}) { workDirectory = cwd, distDirectory = join(cwd, DEFAULT_DIST), skipClearStdout = false, - constantDir = DEFAULT_CONSTANT_DIR_ARR + constantDir = DEFAULT_CONSTANT_DIR_ARR, + disableCopyNpm = false } = options; + // Clean the dist dir before generating + if (existsSync(distDirectory)) { + del.sync(distDirectory + '/**'); + } + if (type === DEFAULT_TYPE) { copyConstantDir(constantDir, distDirectory); } else { @@ -45,7 +52,8 @@ function build(options = {}) { type, workDirectory, distDirectory, - constantDir + constantDir, + disableCopyNpm }); if (options.webpackConfig) { @@ -74,7 +82,8 @@ function watch(options = {}) { workDirectory = cwd, distDirectory = join(cwd, DEFAULT_DIST), skipClearStdout = false, - constantDir = DEFAULT_CONSTANT_DIR_ARR + constantDir = DEFAULT_CONSTANT_DIR_ARR, + disableCopyNpm = false } = options; if (type === DEFAULT_TYPE) { @@ -93,7 +102,8 @@ function watch(options = {}) { workDirectory, platform, distDirectory, - constantDir + constantDir, + disableCopyNpm }); if (options.webpackConfig) { diff --git a/packages/jsx2mp-cli/package.json b/packages/jsx2mp-cli/package.json index bf72f0416c..8556199719 100644 --- a/packages/jsx2mp-cli/package.json +++ b/packages/jsx2mp-cli/package.json @@ -1,6 +1,6 @@ { "name": "jsx2mp-cli", - "version": "0.2.2", + "version": "0.2.3", "license": "BSD-3-Clause", "description": "A cli tool to transform Rax JSX based project to MiniApp.", "bin": { @@ -24,6 +24,7 @@ "chalk": "^2.4.2", "commander": "^2.20.0", "console-clear": "^1.1.1", + "del": "^5.1.0", "fs-extra": "^7.0.1", "inquirer": "^6.2.2", "jsx2mp-loader": "^0.2.0", @@ -31,6 +32,7 @@ "kebab-case": "^1.0.0", "memory-fs": "^0.4.1", "ora": "^3.4.0", + "terser": "^4.3.8", "webpack": "^4.38.0", "webpack-cli": "^3.3.6", "webpack-merge": "^4.2.1" diff --git a/packages/jsx2mp-cli/plugins/runtime.js b/packages/jsx2mp-cli/plugins/runtime.js index 791d9e8342..dd46fe0ab8 100644 --- a/packages/jsx2mp-cli/plugins/runtime.js +++ b/packages/jsx2mp-cli/plugins/runtime.js @@ -1,5 +1,6 @@ const { join } = require('path'); -const { copySync, readJSONSync } = require('fs-extra'); +const { copySync, writeFileSync, readJSONSync, readFileSync } = require('fs-extra'); +const { minify } = require('terser'); /** * Runtime packages should be a dependency of jsx2mp-cli, @@ -11,8 +12,9 @@ const runtimePackageJSON = readJSONSync(runtimePackageJSONPath); const runtimePackagePath = join(runtimePackageJSONPath, '..'); module.exports = class JSX2MPRuntimePlugin { - constructor({ platform = 'ali' }) { + constructor({ platform = 'ali', mode = 'build' }) { this.platform = platform; + this.mode = mode; } apply(compiler) { @@ -25,7 +27,12 @@ module.exports = class JSX2MPRuntimePlugin { const sourceFile = require.resolve(join(runtimePackagePath, runtimeTargetPath)); const targetFile = join(compiler.outputPath, 'npm', runtime + '.js'); - copySync(sourceFile, targetFile); + if (this.mode === 'build') { + const sourceCode = minify(readFileSync(sourceFile, 'utf-8')).code; + writeFileSync(targetFile, sourceCode); + } else { + copySync(sourceFile, targetFile); + } callback(); } ); diff --git a/packages/jsx2mp-loader/package.json b/packages/jsx2mp-loader/package.json index 05b969bd04..1291506e94 100644 --- a/packages/jsx2mp-loader/package.json +++ b/packages/jsx2mp-loader/package.json @@ -1,6 +1,6 @@ { "name": "jsx2mp-loader", - "version": "0.2.2", + "version": "0.2.3", "description": "", "main": "src/app-loader.js", "scripts": { @@ -13,11 +13,16 @@ "@babel/plugin-proposal-class-properties": "^7.5.5", "@babel/plugin-proposal-export-default-from": "^7.5.2", "@babel/preset-env": "^7.5.5", + "babel-plugin-danger-remove-unused-import": "^2.0.0", "babel-plugin-minify-dead-code-elimination": "^0.5.1", "babel-plugin-transform-define": "^1.3.1", "chalk": "^2.4.2", + "csso": "^3.5.1", + "convert-source-map": "^1.6.0", "fs-extra": "^8.1.0", "jsx-compiler": "^0.3.0", - "loader-utils": "^1.2.3" + "loader-utils": "^1.2.3", + "pretty-data": "^0.40.0", + "terser": "^4.3.8" } } diff --git a/packages/jsx2mp-loader/src/app-loader.js b/packages/jsx2mp-loader/src/app-loader.js index ae955bf757..136c7e764a 100644 --- a/packages/jsx2mp-loader/src/app-loader.js +++ b/packages/jsx2mp-loader/src/app-loader.js @@ -1,10 +1,12 @@ -const { readJSONSync, writeJSONSync, writeFileSync, readFileSync, existsSync, mkdirSync } = require('fs-extra'); +const { readJSONSync, readFileSync, existsSync, mkdirSync } = require('fs-extra'); const { join } = require('path'); const compiler = require('jsx-compiler'); const { getOptions } = require('loader-utils'); const moduleResolve = require('./utils/moduleResolve'); const { removeExt } = require('./utils/pathHelper'); +const eliminateDeadCode = require('./utils/dce'); const defaultStyle = require('./defaultStyle'); +const output = require('./output'); function createImportStatement(req) { @@ -32,7 +34,7 @@ function getRelativePath(filePath) { module.exports = function appLoader(content) { const loaderOptions = getOptions(this); - const { entryPath, platform } = loaderOptions; + const { entryPath, platform, mode, disableCopyNpm } = loaderOptions; const appConfigPath = removeExt(this.resourcePath) + '.json'; const rawContent = readFileSync(this.resourcePath, 'utf-8'); const config = readJSONSync(appConfigPath); @@ -46,20 +48,36 @@ module.exports = function appLoader(content) { resourcePath: this.resourcePath, outputPath, sourcePath, + platform, type: 'app', + sourceFileName: this.resourcePath, + disableCopyNpm }); - const transformed = compiler(rawContent, compilerOptions); + const rawContentAfterDCE = eliminateDeadCode(rawContent); + const transformed = compiler(rawContentAfterDCE, compilerOptions); this.addDependency(appConfigPath); const transformedAppConfig = transformAppConfig(entryPath, config); - writeFileSync(join(outputPath, 'app.js'), transformed.code); - writeJSONSync(join(outputPath, 'app.json'), transformedAppConfig, { spaces: 2 }); - // Write app.config.js for route information. - writeFileSync(join(outputPath, 'app.config.js'), `module.exports = ${JSON.stringify(config, null, 2)}`); - const appCssPath = join(outputPath, 'app' + platform.extension.css); - const appCss = transformed.style ? defaultStyle + transformed.style : defaultStyle; - writeFileSync(appCssPath, appCss); + + const outputContent = { + code: transformed.code, + map: transformed.map, + config: `module.exports = ${JSON.stringify(config, null, 2)}`, + css: transformed.style ? defaultStyle + transformed.style : defaultStyle, + json: transformedAppConfig + }; + const outputOption = { + outputPath: { + code: join(outputPath, 'app.js'), + json: join(outputPath, 'app.json'), + css: join(outputPath, 'app' + platform.extension.css), + config: join(outputPath, 'app.config.js') + }, + mode + }; + + output(outputContent, rawContent, outputOption); return [ `/* Generated by JSX2MP AppLoader, sourceFile: ${this.resourcePath}. */`, @@ -77,9 +95,14 @@ function transformAppConfig(entryPath, originalConfig) { const pages = []; if (Array.isArray(value)) { // Only resolve first level of routes. - value.forEach(({ component, source }) => { + value.forEach(({ component, source, targets }) => { // Compatible with old version definition of `component`. - pages.push(moduleResolve(entryPath, getRelativePath(source || component))); + if (!Array.isArray(targets)) { + pages.push(moduleResolve(entryPath, getRelativePath(source || component))); + } + if (Array.isArray(targets) && targets.indexOf('miniapp') > -1) { + pages.push(moduleResolve(entryPath, getRelativePath(source || component))); + } }); } config.pages = pages; diff --git a/packages/jsx2mp-loader/src/babel-plugin-rename-import.js b/packages/jsx2mp-loader/src/babel-plugin-rename-import.js index 6325705212..cb9d2a832f 100644 --- a/packages/jsx2mp-loader/src/babel-plugin-rename-import.js +++ b/packages/jsx2mp-loader/src/babel-plugin-rename-import.js @@ -1,23 +1,20 @@ const { join, relative, dirname } = require('path'); -const { existsSync, statSync } = require('fs-extra'); const chalk = require('chalk'); const { isNpmModule, isWeexModule, isRaxModule } = require('./utils/judgeModule'); +const RUNTIME = 'jsx2mp-runtime'; + +const getRuntimeByPlatform = (platform) => `${RUNTIME}/dist/jsx2mp-runtime.${platform}.esm`; + const defaultOptions = { normalizeNpmFileName: (s) => s, }; -function getNpmName(value) { - const isScopedNpm = /^_?@/.test(value); - return value.split('/').slice(0, isScopedNpm ? 2 : 1).join('/'); -} - module.exports = function visitor({ types: t }, options) { options = Object.assign({}, defaultOptions, options); - const { normalizeNpmFileName, nodeModulesPathList, distSourcePath, outputPath } = options; + const { normalizeNpmFileName, nodeModulesPathList, distSourcePath, outputPath, disableCopyNpm, platform } = options; const source = (value, npmList, filename, rootContext) => { - const npmName = getNpmName(value); // Example: // value => '@ali/universal-goldlog' or '@ali/xxx/foo/lib' // npmList => ['/Users/xxx/node_modules/xxx', '/Users/xxx/node_modules/aaa/node_modules/bbb'] @@ -27,22 +24,9 @@ module.exports = function visitor({ types: t }, options) { const searchPaths = npmList.reverse(); const target = require.resolve(value, { paths: searchPaths }); - // In tnpm, target will be like following (symbol linked path): - // ***/_universal-toast_1.0.0_universal-toast/lib/index.js - let packageJSONPath; - try { - packageJSONPath = require.resolve(join(npmName, 'package.json'), { paths: searchPaths }); - } catch (err) { - throw new Error(`You may not have npm installed: "${npmName}"`); - } - - const moduleBasePath = join(packageJSONPath, '..'); - const modulePathSuffix = relative(moduleBasePath, target); - // ret => '../npm/_ali/universal-goldlog/lib/index.js - const rootNodeModulePath = join(rootContext, 'node_modules'); const filePath = relative(dirname(distSourcePath), join(outputPath, 'npm', relative(rootNodeModulePath, target))); - return t.stringLiteral(normalizeNpmFileName(filePath)); + return t.stringLiteral(normalizeNpmFileName('./' + filePath)); }; return { @@ -52,9 +36,13 @@ module.exports = function visitor({ types: t }, options) { if (isWeexModule(value)) { path.remove(); } else if (isRaxModule(value)) { - const rootNpmRelativePath = relative(dirname(distSourcePath), join(outputPath, 'npm')); - path.node.source = t.stringLiteral('./' + join(rootNpmRelativePath, 'jsx2mp-runtime')); - } else if (isNpmModule(value)) { + let runtimePath = getRuntimeByPlatform(platform.type); + if (!disableCopyNpm) { + const rootNpmRelativePath = relative(dirname(distSourcePath), join(outputPath, 'npm')); + runtimePath = './' + join(rootNpmRelativePath, RUNTIME); + } + path.node.source = t.stringLiteral(runtimePath); + } else if (isNpmModule(value) && !disableCopyNpm) { path.node.source = source(value, nodeModulesPathList, state.filename, state.cwd); } }, @@ -67,11 +55,20 @@ module.exports = function visitor({ types: t }, options) { node.arguments.length === 1 ) { if (t.isStringLiteral(node.arguments[0])) { - if (isWeexModule(node.arguments[0].value)) { - path.replaceWith(t.nullLiteral()); - } else if (isNpmModule(node.arguments[0].value)) { + const moduleName = node.arguments[0].value; + if (isWeexModule(moduleName)) { + path.remove(); + } else if (isRaxModule(moduleName)) { + let runtimePath = t.stringLiteral(getRuntimeByPlatform(platform.type)); + if (!disableCopyNpm) { + runtimePath = source(moduleName, nodeModulesPathList, state.filename, state.cwd); + } + path.node.arguments = [ + runtimePath + ]; + } else if (isNpmModule(moduleName) && !disableCopyNpm) { path.node.arguments = [ - source(node.arguments[0].value, nodeModulesPathList, state.filename, state.cwd) + source(moduleName, nodeModulesPathList, state.filename, state.cwd) ]; } } else if (t.isExpression(node.arguments[0])) { diff --git a/packages/jsx2mp-loader/src/component-loader.js b/packages/jsx2mp-loader/src/component-loader.js index 11a5d8b25b..4839648da2 100644 --- a/packages/jsx2mp-loader/src/component-loader.js +++ b/packages/jsx2mp-loader/src/component-loader.js @@ -1,17 +1,18 @@ -const { readJSONSync, writeJSONSync, writeFileSync, readFileSync, existsSync, mkdirpSync } = require('fs-extra'); -const { relative, join, dirname, sep } = require('path'); +const { readFileSync, existsSync, mkdirpSync } = require('fs-extra'); +const { relative, join, dirname, sep, extname } = require('path'); const compiler = require('jsx-compiler'); const { getOptions } = require('loader-utils'); const cached = require('./cached'); const { removeExt } = require('./utils/pathHelper'); - +const eliminateDeadCode = require('./utils/dce'); +const output = require('./output'); const ComponentLoader = __filename; module.exports = function componentLoader(content) { const loaderOptions = getOptions(this); - const { platform, entryPath, constantDir } = loaderOptions; + const { platform, entryPath, constantDir, mode, disableCopyNpm } = loaderOptions; const rawContent = readFileSync(this.resourcePath, 'utf-8'); const resourcePath = this.resourcePath; const rootContext = this.rootContext; @@ -35,10 +36,13 @@ module.exports = function componentLoader(content) { outputPath, sourcePath, type: 'component', - platform + platform, + sourceFileName: this.resourcePath, + disableCopyNpm }); - const transformed = compiler(rawContent, compilerOptions); + const rawContentAfterDCE = eliminateDeadCode(rawContent); + const transformed = compiler(rawContentAfterDCE, compilerOptions); const config = Object.assign({}, transformed.config); if (Array.isArray(transformed.dependencies)) { @@ -65,25 +69,27 @@ module.exports = function componentLoader(content) { const distFileDir = dirname(distFileWithoutExt); if (!existsSync(distFileDir)) mkdirpSync(distFileDir); - // Write code - writeFileSync(distFileWithoutExt + '.js', transformed.code); - // Write template - writeFileSync(distFileWithoutExt + platform.extension.xml, transformed.template); - // Write config - writeJSONSync(distFileWithoutExt + '.json', config, { spaces: 2 }); - // Write acss style - if (transformed.style) { - writeFileSync(distFileWithoutExt + platform.extension.css, transformed.style); - } - // Write extra assets - if (transformed.assets) { - Object.keys(transformed.assets).forEach((asset) => { - const content = transformed.assets[asset]; - const assetDirectory = dirname(join(outputPath, asset)); - if (!existsSync(assetDirectory)) mkdirpSync(assetDirectory); - writeFileSync(join(outputPath, asset), content); - }); - } + + const outputContent = { + code: transformed.code, + map: transformed.map, + css: transformed.style || '', + json: config, + template: transformed.template, + assets: transformed.assets + }; + const outputOption = { + outputPath: { + code: distFileWithoutExt + '.js', + json: distFileWithoutExt + '.json', + css: distFileWithoutExt + platform.extension.css, + template: distFileWithoutExt + platform.extension.xml, + assets: outputPath + }, + mode + }; + + output(outputContent, rawContent, outputOption); function isCustomComponent(name, usingComponents = {}) { const matchingPath = join(dirname(resourcePath), name); @@ -102,7 +108,11 @@ module.exports = function componentLoader(content) { const denpendencies = []; Object.keys(transformed.imported).forEach(name => { if (isCustomComponent(name, transformed.usingComponents)) { - denpendencies.push({ name, loader: ComponentLoader, options: { entryPath: loaderOptions.entryPath, platform: loaderOptions.platform, constantDir: loaderOptions.constantDir } }); + denpendencies.push({ + name, + loader: ComponentLoader, + options: loaderOptions + }); } else { denpendencies.push({ name }); } diff --git a/packages/jsx2mp-loader/src/output.js b/packages/jsx2mp-loader/src/output.js new file mode 100644 index 0000000000..ec567602ec --- /dev/null +++ b/packages/jsx2mp-loader/src/output.js @@ -0,0 +1,121 @@ +const { writeJSONSync, writeFileSync, existsSync, mkdirpSync } = require('fs-extra'); +const { extname, dirname, join } = require('path'); +const { transformSync } = require('@babel/core'); +const { minify, minifyJS, minifyCSS, minifyXML } = require('./utils/minifyCode'); +const addSourceMap = require('./utils/addSourceMap'); + +function transformCode(rawContent, mode, externalPlugins = [], externalPreset = []) { + const presets = [].concat(externalPreset); + const plugins = externalPlugins.concat([ + require('@babel/plugin-proposal-export-default-from'), // for support of export defualt + [ + require('babel-plugin-transform-define'), + { + 'process.env.NODE_ENV': mode === 'build' ? 'production' : 'development', + } + ], + [ + require('babel-plugin-minify-dead-code-elimination'), + { + optimizeRawSize: true, + keepFnName: true + } + ], + ]); + + + const babelParserOption = { + plugins: [ + 'classProperties', + 'jsx', + 'flow', + 'flowComment', + 'trailingFunctionCommas', + 'asyncFunctions', + 'exponentiationOperator', + 'asyncGenerators', + 'objectRestSpread', + ['decorators', { decoratorsBeforeExport: false }], + 'dynamicImport', + ], // support all plugins + }; + + return transformSync(rawContent, { + presets, + plugins, + parserOpts: babelParserOption + }); +} + +/** + * Process and write file + * @param {object} content Compiled result + * @param {string} raw Original file content + * @param {object} options + */ +function output(content, raw, options) { + const { mode, outputPath, externalPlugins = [] } = options; + let { code, config, json, css, map, template, assets } = content; + if (mode === 'build') { + // Compile ES6 => ES5 and minify code + code = minifyJS(transformCode(code, + mode, + externalPlugins.concat([require('@babel/plugin-proposal-class-properties')]), + [require('@babel/preset-env')] + ).code); + if (config) { + // Compile ES6 => ES5 and minify code + config = minifyJS(transformCode(config, + mode, + externalPlugins.concat([require('@babel/plugin-proposal-class-properties')]), + [require('@babel/preset-env')] + ).code); + } + if (css) { + css = minifyCSS(css); + } + if (template) { + template = minifyXML(template); + } + } else { + code = transformCode(code, mode, externalPlugins).code; + // Add source map + if (map) { + code = addSourceMap(code, raw, map); + } + } + + // Write file + writeFileSync(outputPath.code, code); + if (json) { + writeJSONSync(outputPath.json, json, { spaces: 2}); + } + if (template) { + writeFileSync(outputPath.template, template); + } + if (css) { + writeFileSync(outputPath.css, css); + } + if (config) { + writeFileSync(outputPath.config, config); + } + + // Write extra assets + if (assets) { + Object.keys(assets).forEach((asset) => { + const ext = extname(asset); + let content = assets[asset]; + if (mode === 'build') { + content = minify(content, ext); + } + const assetsOutputPath = join(outputPath.assets, asset); + const assetDirectory = dirname(assetsOutputPath); + if (!existsSync(assetDirectory)) { + mkdirpSync(assetDirectory); + } + writeFileSync(assetsOutputPath, content); + }); + } +} + +module.exports = output; diff --git a/packages/jsx2mp-loader/src/page-loader.js b/packages/jsx2mp-loader/src/page-loader.js index 9113c8ec99..1c4b31a80f 100644 --- a/packages/jsx2mp-loader/src/page-loader.js +++ b/packages/jsx2mp-loader/src/page-loader.js @@ -1,14 +1,16 @@ const { readJSONSync, writeJSONSync, writeFileSync, readFileSync, existsSync, mkdirpSync } = require('fs-extra'); -const { relative, join, dirname } = require('path'); +const { relative, join, dirname, extname } = require('path'); const { getOptions } = require('loader-utils'); const compiler = require('jsx-compiler'); const { removeExt } = require('./utils/pathHelper'); +const eliminateDeadCode = require('./utils/dce'); +const output = require('./output'); const ComponentLoader = require.resolve('./component-loader'); module.exports = function pageLoader(content) { const loaderOptions = getOptions(this); - const { platform, entryPath } = loaderOptions; + const { platform, entryPath, mode, disableCopyNpm } = loaderOptions; const rawContent = readFileSync(this.resourcePath, 'utf-8'); const resourcePath = this.resourcePath; @@ -22,11 +24,13 @@ module.exports = function pageLoader(content) { outputPath, sourcePath, type: 'page', - platform + platform, + sourceFileName: this.resourcePath, + disableCopyNpm }); - - const transformed = compiler(rawContent, compilerOptions); + const rawContentAfterDCE = eliminateDeadCode(rawContent); + const transformed = compiler(rawContentAfterDCE, compilerOptions); const pageDistDir = dirname(targetFilePath); if (!existsSync(pageDistDir)) mkdirpSync(pageDistDir); @@ -54,25 +58,26 @@ module.exports = function pageLoader(content) { config.usingComponents = usingComponents; } - // Write js content - writeFileSync(distFileWithoutExt + '.js', transformed.code); - // Write template - writeFileSync(distFileWithoutExt + platform.extension.xml, transformed.template); - // Write config - writeJSONSync(distFileWithoutExt + '.json', config, { spaces: 2 }); - // Write acss style - if (transformed.style) { - writeFileSync(distFileWithoutExt + platform.extension.css, transformed.style); - } - // Write extra assets - if (transformed.assets) { - Object.keys(transformed.assets).forEach((asset) => { - const content = transformed.assets[asset]; - const assetDirectory = dirname(join(outputPath, asset)); - if (!existsSync(assetDirectory)) mkdirpSync(assetDirectory); - writeFileSync(join(outputPath, asset), content); - }); - } + const outputContent = { + code: transformed.code, + map: transformed.map, + css: transformed.style || '', + json: config, + template: transformed.template, + assets: transformed.assets + }; + const outputOption = { + outputPath: { + code: distFileWithoutExt + '.js', + json: distFileWithoutExt + '.json', + css: distFileWithoutExt + platform.extension.css, + template: distFileWithoutExt + platform.extension.xml, + assets: outputPath + }, + mode + }; + + output(outputContent, rawContent, outputOption); function isCustomComponent(name, usingComponents = {}) { const matchingPath = join(dirname(resourcePath), name); @@ -88,7 +93,11 @@ module.exports = function pageLoader(content) { const denpendencies = []; Object.keys(transformed.imported).forEach(name => { if (isCustomComponent(name, transformed.usingComponents)) { - denpendencies.push({ name, loader: ComponentLoader, options: { entryPath: loaderOptions.entryPath, platform: loaderOptions.platform, constantDir: loaderOptions.constantDir } }); + denpendencies.push({ + name, + loader: ComponentLoader, + options: loaderOptions + }); } else { denpendencies.push({ name }); } diff --git a/packages/jsx2mp-loader/src/script-loader.js b/packages/jsx2mp-loader/src/script-loader.js index 9e6513575b..193e23d140 100644 --- a/packages/jsx2mp-loader/src/script-loader.js +++ b/packages/jsx2mp-loader/src/script-loader.js @@ -1,12 +1,12 @@ const { join, dirname, relative } = require('path'); -const { copySync, lstatSync, existsSync, mkdirpSync, writeJSONSync, writeFileSync, readFileSync, readJSONSync } = require('fs-extra'); +const { copySync, existsSync, mkdirpSync, writeJSONSync, writeFileSync, readFileSync, readJSONSync } = require('fs-extra'); const { transformSync } = require('@babel/core'); const { getOptions } = require('loader-utils'); const cached = require('./cached'); const isMiniappComponent = require('./utils/isMiniappComponent'); const { removeExt } = require('./utils/pathHelper'); const { isNpmModule } = require('./utils/judgeModule'); - +const output = require('./output'); const AppLoader = require.resolve('./app-loader'); const PageLoader = require.resolve('./page-loader'); @@ -21,6 +21,7 @@ module.exports = function scriptLoader(content) { if (loaderHandled) return; const loaderOptions = getOptions(this); + const { disableCopyNpm, mode, entryPath, platform } = loaderOptions; const rawContent = readFileSync(this.resourcePath, 'utf-8'); const rootContext = this.rootContext; const nodeModulesPathList = getNearestNodeModulesPath(rootContext, this.resourcePath); @@ -39,6 +40,9 @@ module.exports = function scriptLoader(content) { }); if (isFromNodeModule(this.resourcePath)) { + if (disableCopyNpm) { + return ''; + } const relativeNpmPath = relative(currentNodeModulePath, this.resourcePath); const npmFolderName = getNpmFolderName(relativeNpmPath); const sourcePackagePath = join(currentNodeModulePath, npmFolderName); @@ -91,96 +95,53 @@ module.exports = function scriptLoader(content) { splitedNpmPath.shift(); // Skip npm module package, for cnpm/tnpm will rewrite this. const distSourcePath = normalizeNpmFileName(join(outputPath, 'npm', relative(rootNodeModulePath, this.resourcePath))); - const { code, map } = transformCode({rawContent, mode: loaderOptions.mode, nodeModulesPathList, relativeResourcePath, distSourcePath, outputPath}); - const distSourceDirPath = dirname(distSourcePath); if (!existsSync(distSourceDirPath)) mkdirpSync(distSourceDirPath); - writeFileSync(distSourcePath, code, 'utf-8'); - writeJSONSync(distSourcePath + '.map', map); + + const outputContent = { code: rawContent }; + const outputOption = { + outputPath: { + code: distSourcePath + }, + mode, + externalPlugins: [ + [ + require('./babel-plugin-rename-import'), + { normalizeNpmFileName, nodeModulesPathList, distSourcePath, outputPath, disableCopyNpm, platform } + ] + ] + }; + output(outputContent, null, outputOption); } } else { const relativeFilePath = relative( - join(rootContext, dirname(loaderOptions.entryPath)), + join(rootContext, dirname(entryPath)), this.resourcePath ); const distSourcePath = join(outputPath, relativeFilePath); const distSourceDirPath = dirname(distSourcePath); - const { code } = transformCode({rawContent, mode: loaderOptions.mode, nodeModulesPathList, relativeResourcePath, distSourcePath, outputPath}); - if (!existsSync(distSourceDirPath)) mkdirpSync(distSourceDirPath); - writeFileSync(distSourcePath, code, 'utf-8'); + const outputContent = { code: rawContent }; + const outputOption = { + outputPath: { + code: distSourcePath + }, + mode, + externalPlugins: [ + [ + require('./babel-plugin-rename-import'), + { normalizeNpmFileName, nodeModulesPathList, distSourcePath, outputPath, disableCopyNpm, platform } + ] + ] + }; + + output(outputContent, null, outputOption); } return content; }; -/** - * - * @param {object} option - * @param {string} option.rawContent code to be transformed - * @param {string} option.mode transform mode, build or watch - * @param {array} option.nodeModulesPathList existed node_modules paths - * @param {array} option.relativeResourcePath current file path relative to rootContext - * @param {array} option.distSourcePath file path that transformed to - * @param {array} option.outputPath dist dir path - * - */ -function transformCode({rawContent, mode, nodeModulesPathList = [], relativeResourcePath, distSourcePath, outputPath}) { - const presets = []; - const plugins = [ - [ - require('./babel-plugin-rename-import'), - { normalizeNpmFileName, nodeModulesPathList, distSourcePath, outputPath } - - ], // for rename npm modules. - require('@babel/plugin-proposal-export-default-from'), // for support of export defualt - [ - require('babel-plugin-transform-define'), - { - 'process.env.NODE_ENV': mode === 'build' ? 'production' : 'development', - } - ], - [ - require('babel-plugin-minify-dead-code-elimination'), - { - optimizeRawSize: true, - keepFnName: true - } - ] - ]; - - // Compile to ES5 for build. - if (mode === 'build') { - presets.push(require('@babel/preset-env')); - plugins.push(require('@babel/plugin-proposal-class-properties')); - } - - const babelParserOption = { - plugins: [ - 'classProperties', - 'jsx', - 'flow', - 'flowComment', - 'trailingFunctionCommas', - 'asyncFunctions', - 'exponentiationOperator', - 'asyncGenerators', - 'objectRestSpread', - ['decorators', { decoratorsBeforeExport: false }], - 'dynamicImport', - ], // support all plugins - }; - - return transformSync(rawContent, { - presets, - plugins, - filename: relativeResourcePath, - parserOpts: babelParserOption, - sourceMaps: true - }); -} - /** * For that alipay build folder can not contain `@`, escape to `_`. */ diff --git a/packages/jsx2mp-loader/src/utils/addSourceMap.js b/packages/jsx2mp-loader/src/utils/addSourceMap.js new file mode 100644 index 0000000000..8092eba843 --- /dev/null +++ b/packages/jsx2mp-loader/src/utils/addSourceMap.js @@ -0,0 +1,11 @@ +const convertSourceMap = require('convert-source-map'); + +function addSourceMap(code, rawCode, originalMap) { + const map = Object.assign(originalMap, { + sourcesContent: [rawCode] + }); + const sourceMapString = convertSourceMap.fromObject(map).toComment(); + return code + '\n' + sourceMapString; +} + +module.exports = addSourceMap; diff --git a/packages/jsx2mp-loader/src/utils/dce.js b/packages/jsx2mp-loader/src/utils/dce.js new file mode 100644 index 0000000000..870729d35e --- /dev/null +++ b/packages/jsx2mp-loader/src/utils/dce.js @@ -0,0 +1,40 @@ +const { transformSync } = require('@babel/core'); + +const parserOpts = { + plugins: [ + 'classProperties', + 'jsx', + 'flow', + 'flowComment', + 'trailingFunctionCommas', + 'asyncFunctions', + 'exponentiationOperator', + 'asyncGenerators', + 'objectRestSpread', + ['decorators', { decoratorsBeforeExport: false }], + 'dynamicImport', + ], // support all plugins +}; + +function eliminateDeadCode(source) { + return transformSync(source, { + parserOpts, + plugins: [ + [ + require('babel-plugin-minify-dead-code-elimination'), + { + optimizeRawSize: true, + keepFnName: true + } + ], + [ + require('babel-plugin-danger-remove-unused-import'), + { + ignore: 'rax' + } + ] + ] + }).code; +} + +module.exports = eliminateDeadCode; diff --git a/packages/jsx2mp-loader/src/utils/minifyCode.js b/packages/jsx2mp-loader/src/utils/minifyCode.js new file mode 100644 index 0000000000..4aa9da61c2 --- /dev/null +++ b/packages/jsx2mp-loader/src/utils/minifyCode.js @@ -0,0 +1,46 @@ +const terser = require('terser'); +const csso = require('csso'); +const prettyData = require('pretty-data').pd; + +function minifyJS(source) { + return terser.minify(source).code; +} + +function minifyCSS(source) { + return csso.minify(source, { + restructure: false + }).css; +} + +function minifyXML(source) { + return prettyData.xmlmin(source); +} + +function minifyJSON(source) { + return prettyData.json(source); +} + +function minify(source, type = '.js') { + if (type === '.js') { + return minifyJS(source); + } + if (type === '.css') { + return minifyCSS(source); + } + if (type === '.json') { + return minifyJSON(source); + } + if (/\..*ml/.test(type)) { + return minifyXML(source); + } + + return source; +} + +module.exports = { + minify, + minifyJS, + minifyCSS, + minifyXML, + minifyJSON +}; diff --git a/packages/jsx2mp-runtime/package.json b/packages/jsx2mp-runtime/package.json index a9bc21958b..f2858047ad 100644 --- a/packages/jsx2mp-runtime/package.json +++ b/packages/jsx2mp-runtime/package.json @@ -1,6 +1,6 @@ { "name": "jsx2mp-runtime", - "version": "0.3.1", + "version": "0.3.2", "description": "Runtime for jsx2mp.", "miniprogram": { "ali": "dist/jsx2mp-runtime.ali.esm.js", diff --git a/packages/jsx2mp-runtime/src/bridge.js b/packages/jsx2mp-runtime/src/bridge.js index 474e597fa1..3acd265320 100644 --- a/packages/jsx2mp-runtime/src/bridge.js +++ b/packages/jsx2mp-runtime/src/bridge.js @@ -2,7 +2,7 @@ import { cycles as appCycles } from './app'; import Component from './component'; import { ON_SHOW, ON_HIDE, ON_PAGE_SCROLL, ON_SHARE_APP_MESSAGE, ON_REACH_BOTTOM, ON_PULL_DOWN_REFRESH } from './cycles'; -import { setComponentInstance, getComponentProps, executeCallbacks } from './updater'; +import { setComponentInstance, getComponentProps } from './updater'; import { getComponentLifecycle } from '@@ADAPTER@@'; import { createMiniAppHistory } from './history'; import { __updateRouterMap } from './router'; @@ -68,7 +68,6 @@ function getComponentCycles(Klass) { if (this[PROPS].hasOwnProperty('__tagId')) { const componentId = this[PROPS].__tagId; setComponentInstance(componentId, this.instance); - executeCallbacks(); } if (GET_DERIVED_STATE_FROM_PROPS in Klass) { diff --git a/packages/jsx2mp-runtime/src/component.js b/packages/jsx2mp-runtime/src/component.js index ce72d29797..ad865215c7 100644 --- a/packages/jsx2mp-runtime/src/component.js +++ b/packages/jsx2mp-runtime/src/component.js @@ -30,6 +30,7 @@ export default class Component { this.__dependencies = {}; // for context + this.__mounted = false; this.__shouldUpdate = false; this._methods = {}; this._hooks = {}; @@ -99,6 +100,24 @@ export default class Component { updateChildProps(this, chlidInstanceId, props); } + _registerRefs(refs) { + this.refs = {}; + refs.forEach(({name, method}) => { + if (!method) { + const target = { + current: null + }; + this._internal[name] = ref => { + target.current = ref; + }; + this.refs[name] = target; + } else { + this._internal[name] = method; + this.refs[name] = method; + } + }); + } + _collectState() { const state = Object.assign({}, this.state); let parialState; @@ -166,7 +185,6 @@ export default class Component { // Step4: mark __mounted = true if (!this.__mounted) { this.__mounted = true; - // Step5: trigger did mount this._trigger(COMPONENT_DID_MOUNT); } @@ -178,12 +196,11 @@ export default class Component { _updateComponent() { if (!this.__mounted) return; - // Step1: propTypes check, now skipped. // Step2: make props to prevProps, and trigger willReceiveProps const nextProps = this.props; // actually this is nextProps const prevProps = this.props = this.prevProps || this.props; - if (this.__mounted && diffProps(prevProps, nextProps)) { + if (diffProps(prevProps, nextProps)) { this._trigger(COMPONENT_WILL_RECEIVE_PROPS, this.props); } @@ -202,18 +219,16 @@ export default class Component { if (stateFromProps !== undefined) nextState = stateFromProps; // Step5: judge shouldComponentUpdate - if (this.__mounted) { - this.__shouldUpdate = true; - if ( - !this.__forceUpdate - && this.shouldComponentUpdate - && this.shouldComponentUpdate(nextProps, nextState) === false - ) { - this.__shouldUpdate = false; - } else { - // Step6: trigger will update - this._trigger(COMPONENT_WILL_UPDATE, nextProps, nextState); - } + this.__shouldUpdate = true; + if ( + !this.__forceUpdate + && this.shouldComponentUpdate + && this.shouldComponentUpdate(nextProps, nextState) === false + ) { + this.__shouldUpdate = false; + } else { + // Step6: trigger will update + this._trigger(COMPONENT_WILL_UPDATE, nextProps, nextState); } this.props = nextProps; diff --git a/packages/jsx2mp-runtime/src/enqueueRender.js b/packages/jsx2mp-runtime/src/enqueueRender.js index 503df46808..6b6868ebd6 100644 --- a/packages/jsx2mp-runtime/src/enqueueRender.js +++ b/packages/jsx2mp-runtime/src/enqueueRender.js @@ -1,5 +1,4 @@ import nextTick from './nextTick'; -import { RENDER } from './cycles'; let queue = []; diff --git a/packages/jsx2mp-runtime/src/hooks.js b/packages/jsx2mp-runtime/src/hooks.js index ef3ed8df8e..dd1c739a65 100644 --- a/packages/jsx2mp-runtime/src/hooks.js +++ b/packages/jsx2mp-runtime/src/hooks.js @@ -177,9 +177,15 @@ export function useRef(initialValue) { const hooks = currentInstance.getHooks(); if (!hooks[hookID]) { - hooks[hookID] = { + const ref = { current: initialValue }; + const refFn = (instance) => { + ref.current = instance; + }; + refFn.__proto__ = ref; + // currentInstance._internal., + hooks[hookID] = refFn; } return hooks[hookID]; diff --git a/packages/jsx2mp-runtime/src/updater.js b/packages/jsx2mp-runtime/src/updater.js index b3122fb3c7..be23a98024 100644 --- a/packages/jsx2mp-runtime/src/updater.js +++ b/packages/jsx2mp-runtime/src/updater.js @@ -1,14 +1,20 @@ import nextTick from './nextTick'; +import { enqueueRender } from './enqueueRender'; const propsMap = { // tagId -> props }; const componentIntances = {}; -const updateChildPropsCallbacks = []; +const updateChildPropsCallbacks = {}; export function setComponentInstance(instanceId, instance) { componentIntances[instanceId] = instance; + // Check component should update chlid props + if (updateChildPropsCallbacks[instanceId]) { + updateChildPropsCallbacks[instanceId](); + updateChildPropsCallbacks[instanceId] = null; + } } export function getComponentProps(tagId) { @@ -23,8 +29,8 @@ export function removeComponentProps(tagId) { } export function updateChildProps(trigger, instanceId, nextProps) { - const targetComponent = componentIntances[instanceId]; if (trigger) { + const targetComponent = componentIntances[instanceId]; // Create a new object reference. if (targetComponent) { propsMap[instanceId] = Object.assign( @@ -33,21 +39,19 @@ export function updateChildProps(trigger, instanceId, nextProps) { nextProps, ); nextTick(() => { - Object.assign(targetComponent.props, propsMap[instanceId]); - targetComponent._updateComponent(); + targetComponent.props = propsMap[instanceId]; + enqueueRender(targetComponent); }); } else { /** * updateChildProps may execute before setComponentInstance */ - updateChildPropsCallbacks.push( - updateChildProps.bind(null, trigger, instanceId, nextProps), + updateChildPropsCallbacks[instanceId] = updateChildProps.bind( + null, + trigger, + instanceId, + nextProps, ); } } } - -export function executeCallbacks() { - updateChildPropsCallbacks.forEach(callback => callback()); - updateChildPropsCallbacks.length = 0; -}