diff --git a/packages/wxa-cli/package-lock.json b/packages/wxa-cli/package-lock.json index ffa66924..f87de0fb 100644 --- a/packages/wxa-cli/package-lock.json +++ b/packages/wxa-cli/package-lock.json @@ -1,6 +1,6 @@ { "name": "@wxa/cli2", - "version": "2.0.4", + "version": "2.0.5", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/packages/wxa-cli/src/builder.js b/packages/wxa-cli/src/builder.js index 6267c9f6..baf9b7cc 100644 --- a/packages/wxa-cli/src/builder.js +++ b/packages/wxa-cli/src/builder.js @@ -1,7 +1,6 @@ -import {readFile, applyPlugins, isFile} from './utils'; +import {readFile, applyPlugins, isFile, getHash} from './utils'; import path from 'path'; -import fs, {unlink} from 'fs'; -import crypto from 'crypto'; +import fs from 'fs'; import chokidar from 'chokidar'; import globby from 'globby'; import debugPKG from 'debug'; @@ -16,6 +15,7 @@ import {AsyncParallelHook, SyncBailHook, AsyncSeriesHook} from 'tapable'; import DependencyResolver from './helpers/dependencyResolver'; import root from './const/root'; import ProgressTextBar from './helpers/progressTextBar'; +import color from './const/color'; let debug = debugPKG('WXA:Builder'); class Builder { @@ -67,17 +67,17 @@ class Builder { return this.loader.mount(this.wxaConfigs.use, cmd); } - filterModule(arr) { - return arr.reduce((ret, dep)=>{ + filterModule(indexedMap) { + let ret = []; + indexedMap.forEach((dep)=>{ if ( !/src\/_wxa/.test(dep.src) ) { ret.push(dep.src); } + }); - - return ret; - }, []); + return ret; } watch(cmd) { @@ -105,22 +105,21 @@ class Builder { logger.warn('change', filepath); debug('WATCH file changed %s', filepath); - let mdl = this.schedule.$indexOfModule.find((module)=>module.src===filepath); + let mdl = this.schedule.$indexOfModule.get(filepath); let isChange = true; debug('Changed Module %O', mdl); // module with code; if (!mdl.isFile) { - let content = readFile(mdl.src); - debug('changed content %s', content); - let md5 = crypto.createHash('md5').update(content).digest('hex'); - - mdl.content = content; - mdl.code = void(0); - isChange = mdl.hash !== md5; - debug('OLD HASH %s, NEW HASH %s', mdl.hash, md5); + let hash = getHash(filepath); + isChange = mdl.hash !== hash; + debug('OLD HASH %s, NEW HASH %s', mdl.hash, hash); } if (isChange) { + mdl.color = color.CHANGED; + mdl.content = void(0); + mdl.code = void(0); + let changedDeps; try { this.schedule.$depPending.push(mdl); @@ -129,8 +128,6 @@ class Builder { changedDeps = await this.schedule.$doDPA(); await this.optimizeAndGenerate(changedDeps, cmd); - // logger.log('Done', '编译完成'); - // debug('schedule dependencies Tree is %O', this.schedule.$indexOfModule); } catch (e) { logger.error('编译失败', e); } @@ -217,14 +214,7 @@ class Builder { await this.schedule.doDPA(); this.schedule.perf.show(); - debug('schedule dependencies Tree is %O', this.schedule.$indexOfModule.map((item)=>{ - delete item.ast; - delete item.xml; - - if (item.reference) delete item.reference; - - return item; - })); + debug('schedule dependencies Tree is %O', this.schedule.$indexOfModule); await this.optimizeAndGenerate(this.schedule.$indexOfModule, cmd); @@ -244,25 +234,27 @@ class Builder { * optimize all module in list and generate dest file. * optiming and generating is parallel. * - * @param {Array} list + * @param {Array} indexedMap * @param {Object} cmdOptions cmd options */ - async optimizeAndGenerate(list, cmdOptions) { + async optimizeAndGenerate(indexedMap, cmdOptions) { try { // module optimize, dependencies merge, minor. let optimizer = new Optimizer(this.current, this.wxaConfigs, cmdOptions); applyPlugins(this.schedule.wxaConfigs.plugins, optimizer); - let optimizeTasks = list.map((dep)=>{ - return optimizer.do(dep); + let optimizeTasks = []; + indexedMap.forEach((dep)=>{ + optimizeTasks.push(optimizer.do(dep)); }); await Promise.all(optimizeTasks); // write module to dest, dependencies copy. let generator = new Generator(this.current, this.schedule.meta, this.wxaConfigs, cmdOptions); - let generateTasks = list.map((mdl)=>{ - return generator.do(mdl); + let generateTasks = []; + indexedMap.forEach((mdl)=>{ + generateTasks.push(generator.do(mdl)); }); await Promise.all(generateTasks); diff --git a/packages/wxa-cli/src/compilers/index.js b/packages/wxa-cli/src/compilers/index.js index 836993fc..e2dbf223 100644 --- a/packages/wxa-cli/src/compilers/index.js +++ b/packages/wxa-cli/src/compilers/index.js @@ -23,12 +23,18 @@ const jsOptions = { * @class EmptyCompiler */ export default class Compiler { - constructor(resolve, meta, appConfigs) { + constructor(resolve, meta, appConfigs, scheduler) { this.current = meta.current; - this.configs = {}; this.resolve = resolve; this.meta = meta; this.appConfigs = appConfigs; + + this.$scheduer = scheduler; + } + + destroy() { + this.appConfigs = null; + this.$scheduer = null; } async parse(mdl) { @@ -131,8 +137,8 @@ export default class Compiler { } default: { - // return Promise.reject(`未识别的文件类型%{type}, 请检查是否添加指定的loader`); - return Promise.resolve({kind: 'other'}); + // unknown type, can be define by other compiler. + return Promise.resolve({kind: mdl.sourceType || 'other'}); } } } @@ -184,6 +190,12 @@ export default class Compiler { children = new ComponentManager(this.resolve, this.meta, this.appConfigs).parse(mdl); } + if (mdl.src === this.$scheduer.APP_CONFIG_PATH) { + // global components in wxa; + // delete custom field in app.json or wechat devtool will get wrong. + delete mdl.json['wxa.globalComponents']; + } + mdl.code = JSON.stringify(mdl.json, void(0), 4); return children; diff --git a/packages/wxa-cli/src/const/root.js b/packages/wxa-cli/src/const/root.js index e867b1cf..a5e84307 100644 --- a/packages/wxa-cli/src/const/root.js +++ b/packages/wxa-cli/src/const/root.js @@ -4,5 +4,6 @@ export default { kind: 'root', isAbstract: true, isROOT: true, - childNodes: [], + childNodes: new Map(), + package: '', // 主包 }; diff --git a/packages/wxa-cli/src/generator/index.js b/packages/wxa-cli/src/generator/index.js index b5902aa5..33024cdf 100644 --- a/packages/wxa-cli/src/generator/index.js +++ b/packages/wxa-cli/src/generator/index.js @@ -3,6 +3,7 @@ import {writeFile, getDistPath, readFile, copy} from '../utils'; import debugPKG from 'debug'; import DependencyResolver from '../helpers/dependencyResolver'; import ProgressBar from '../helpers/progressTextBar'; +import logger from '../helpers/logger'; let debug = debugPKG('WXA:Generator'); @@ -24,35 +25,47 @@ export default class Generator { this.progress.draw(text, 'Generating', !this.cmdOptions.verbose); debug('module to generate %O', mdl); - let outputPath; - if (!mdl.meta || !mdl.meta.outputPath) { - let dr = new DependencyResolver(this.resolve, this.meta); - outputPath = dr.getOutputPath(mdl.src, mdl.pret, mdl); - } else { - outputPath = mdl.meta.outputPath; + // let outputPath; + // if (!mdl.meta || !mdl.meta.outputPath) { + // let dr = new DependencyResolver(this.resolve, this.meta); + // outputPath = dr.getOutputPath(mdl.src, mdl.pret, mdl); + // } else { + // outputPath = mdl.meta.outputPath; + // } + if (!mdl.output) { + logger.errors('module 数据结构有问题', mdl); + return; } - outputPath = this.tryTransFormExtension(outputPath); - debug('transform ext %s', outputPath); - mdl.meta.accOutputPath = outputPath; - - if (mdl.isFile) { - copy(mdl.src, outputPath); - } else { - writeFile(outputPath, mdl.code); - } + mdl.output.forEach((info, outputPath)=>{ + outputPath = this.tryTransFormExtension(outputPath, info.reference.kind); + debug('transform ext %s', outputPath); + info.reality = outputPath; + if (mdl.isFile) { + copy(mdl.src, outputPath); + } else { + writeFile(outputPath, mdl.code); + } + }); } - tryTransFormExtension(output) { + tryTransFormExtension(output, kind) { if (this.wxaConfigs.target === 'wxa') { // 小程序相关 let opath = path.parse(output); let ext; - switch (opath.ext) { - case '.css': ext = '.wxss'; break; - case '.xml': ext = '.wxml'; break; - case '.ts': ext = '.js'; break; + switch (kind || opath.ext) { + case '.css': + case 'css': + ext = '.wxss'; break; + case '.xml': + case 'xml': + ext = '.wxml'; break; + case '.ts': + case 'ts': + case 'typescript': + ext = '.js'; break; default: ext = opath.ext; } diff --git a/packages/wxa-cli/src/helpers/dependencyResolver.js b/packages/wxa-cli/src/helpers/dependencyResolver.js index 04fbaa40..7de33bb1 100644 --- a/packages/wxa-cli/src/helpers/dependencyResolver.js +++ b/packages/wxa-cli/src/helpers/dependencyResolver.js @@ -102,8 +102,8 @@ class DependencyResolver { getOutputPath(source, pret, mdl) { if (pret.isRelative || pret.isAPPAbsolute || pret.isNodeModule || pret.isWXALib) { let opath = pret.isWXALib ? - path.parse(path.join(this.meta.context, '_wxa', pret.name+pret.ext)) : - path.parse(source); + path.parse(path.join(this.meta.context, '_wxa', pret.name+pret.ext)) : + path.parse(source); return this.getDistPath(opath); } else if (pret.isPlugin || pret.isURI) { @@ -124,7 +124,7 @@ class DependencyResolver { let fileOutputPath = ( mdl.meta && mdl.meta.outputPath || - this.getDistPath(path.parse(mdl.src)) + this.getDistPath(path.parse(mdl.src), mdl) ); resolved = './'+path.relative(path.parse(fileOutputPath).dir, libOutputPath); @@ -145,18 +145,18 @@ class DependencyResolver { return content; } - getDistPath(opath) { + getDistPath(absPath, mdl) { let relative; - opath = typeof opath === 'string' ? path.parse(opath) : opath; + absPath = typeof absPath === 'string' ? path.parse(absPath) : absPath; - if (path.relative(this.meta.current, opath.dir).indexOf('node_modules') === 0) { - relative = path.relative(path.join(this.meta.current, 'node_modules'), opath.dir); - relative = path.join('npm', relative); + if (path.relative(this.meta.current, absPath.dir).indexOf('node_modules') === 0) { + relative = path.relative(path.join(this.meta.current, 'node_modules'), absPath.dir); + relative = path.join(mdl.package, 'npm', relative); } else { - relative = path.relative(this.meta.context, opath.dir); + relative = path.relative(this.meta.context, absPath.dir); } - return path.join(this.meta.output.path, relative, opath.base); + return path.join(this.meta.output.path, relative, absPath.base); } } diff --git a/packages/wxa-cli/src/resolvers/ast/index.js b/packages/wxa-cli/src/resolvers/ast/index.js index 53150dd2..c8c38c38 100644 --- a/packages/wxa-cli/src/resolvers/ast/index.js +++ b/packages/wxa-cli/src/resolvers/ast/index.js @@ -118,7 +118,7 @@ export default class ASTManager { src: source, pret: pret, meta: { - source, outputPath, + source, outputPath, resolved, }, }); @@ -150,6 +150,7 @@ export default class ASTManager { libs = libs.concat(wxaSourceLibs); // generate module code. mdl.code = this.generate(mdl).code; + delete mdl.ast; return libs; } /** diff --git a/packages/wxa-cli/src/schedule.js b/packages/wxa-cli/src/schedule.js index 02767961..ae635566 100644 --- a/packages/wxa-cli/src/schedule.js +++ b/packages/wxa-cli/src/schedule.js @@ -64,9 +64,10 @@ class Schedule { }, }); - this.$pageArray = []; // denpendencies + this.$pageArray = new Map(); // denpendencies this.$depPending = []; // pending dependencies - this.$indexOfModule = [ROOT]; // all module + // this.$indexOfModule = [ROOT]; + this.$indexOfModule = new Map([['__root__', ROOT]]); // all module this.$isMountingCompiler = false; // if is mounting compiler, all task will be blocked. this.progress = new ProgressTextBar(this.current, wxaConfigs); @@ -98,7 +99,7 @@ class Schedule { addEntryPoint(mdl) { let child = this.findOrAddDependency(mdl, ROOT); - if (!~ROOT.childNodes.findIndex((mdl)=>mdl.src===child.src)) ROOT.childNodes.push(child); + if (!~ROOT.childNodes.has(mdl.src)) ROOT.childNodes.set(child.src, child); return child; } @@ -106,7 +107,7 @@ class Schedule { async doDPA() { if (!this.$depPending.length) { logger.error('找不到可编译的入口文件'); - return; + throw new Error('找不到可编译的入口文件'); } debug('depPending %o', this.$depPending); @@ -115,7 +116,7 @@ class Schedule { return this.$doDPA(); } - $doDPA() { + async $doDPA() { let tasks = []; // while (this.$depPending.length) { let dep = this.$depPending.shift(); @@ -124,16 +125,16 @@ class Schedule { tasks.push(this.$parse(dep)); // } - return Promise.all(tasks).then(async (succ)=>{ - if (this.$depPending.length === 0) { - // dependencies resolve complete - this.progress.clean(); - return Promise.resolve(succ); - } else { - let sub = await this.$doDPA(); - return succ.concat(sub); - } - }); + let succ = await Promise.all(tasks); + + if (this.$depPending.length === 0) { + // dependencies resolve complete + this.progress.clean(); + return succ; + } else { + let sub = await this.$doDPA(); + return succ.concat(sub); + } } async $parse(dep) { @@ -148,9 +149,7 @@ class Schedule { const text = this.cmdOptions.verbose ? `(Hash: ${dep.hash}) ${relativeSrc}` : relativeSrc; this.progress.draw(text, 'COMPILING', !this.cmdOptions.verbose); - this.perf.markStart(relativeSrc); - this.hooks.buildModule.call(dep); // loader: use custom compiler to load resource. await this.loader.compile(dep); @@ -163,25 +162,24 @@ class Schedule { // Todo: conside if cache is necessary here. // debug('dep to process %O', dep); - let compiler = new Compiler(this.wxaConfigs.resolve, this.meta, this.appConfigs); + let compiler = new Compiler(this.wxaConfigs.resolve, this.meta, this.appConfigs, this); let childNodes = await compiler.parse(dep); + compiler.destroy(); + debug('childNodes', childNodes.map((node)=>simplify(node))); let children = childNodes.reduce((children, node)=>{ let child = this.findOrAddDependency(node, dep); - if (child) return children.concat(child); - - return children; + return child ? children.concat([child.src, child]) : [children.src, children]; }, []); - // if watch mode, use childNodes to clean up the dep tree. // update each module's childnodes, then according to reference unlink file. this.cleanUpChildren(children, dep); // cover new childNodes - dep.childNodes = new Set(children); + dep.childNodes = new Map(children); dep.color = COLOR.COMPILED; // if module is app.json, then add Page entry points. @@ -189,15 +187,7 @@ class Schedule { this.appConfigs = {...dep.json}; debug('app configs is %O', dep.json); - if (dep.json['wxa.globalComponents']) { - // global components in wxa; - // delete custom field in app.json or wechat devtool will get wrong. - delete dep.json['wxa.globalComponents']; - - dep.code = JSON.stringify(dep.json, void(0), 4); - } - - let oldPages = this.$pageArray.slice(0); + let oldPages = new Map(this.$pageArray.entries()); let newPages = this.addPageEntryPoint(); this.cleanUpPages(newPages, oldPages); } @@ -216,61 +206,63 @@ class Schedule { cleanUpChildren(newChildren, mdl) { debug('clean up module %O', simplify(mdl)); - if (mdl.childNodes == null) return; - - mdl.childNodes.forEach((oldChild)=>{ - if (!~newChildren.findIndex((item)=>item.src === oldChild.src)) { - // child node not used, update reference. - debug('denpendencies clean up started'); - - if (oldChild.reference == null) { - debug('Error: old child node\'s reference is no find %O', simplify(oldChild) ); - return; - } - - let idxOfParent = oldChild.reference.findIndex((ref)=>ref.src === mdl.src); - debug('find index %s', idxOfParent); - - if (idxOfParent === -1) { - debug('Error: do not find parent module'); - return; - } - - oldChild.reference.splice(idxOfParent, 1); - - // debug('oldChild %O', oldChild); - - if (oldChild.reference.length === 0 && !oldChild.isROOT) { - debug('useless module find %s', oldChild.src); - - // nested clean children - this.cleanUpChildren([], oldChild); - // unlink module - oldChild.meta && oldChild.meta.accOutputPath && unlinkSync(oldChild.meta.accOutputPath); - this.$indexOfModule.splice(this.$indexOfModule.findIndex((mdl)=>mdl.src===oldChild.src), 1); - } + if ( + mdl.childNodes == null || + mdl.childNodes.size === 0 + ) return; + + mdl.childNodes.forEach((oldChild, src)=>{ + if ( + newChildren.has(src) || + !oldChild.reference.has(mdl.src) + ) return; + // child node not used, update reference. + debug('denpendencies clean up started'); + + oldChild.reference.delete(src); + + if ( + oldChild.reference.size === 0 && + !oldChild.isROOT + ) { + debug('useless module find %s', oldChild.src); + // nested clean children + this.cleanUpChildren([], oldChild); + // unlink module + this.deleteFile(oldChild); + this.$indexOfModule.delete(src); } }); } cleanUpPages(newPages, oldPages) { - let droppedPages = oldPages.filter((oldPage)=>newPages.findIndex((page)=>page.src===oldPage.src)===-1); + let droppedPages = []; + oldPages.forEach((oldPage)=>{ + if (newPages.has(oldPage.src)) droppedPages.push(oldPage); + }); droppedPages.forEach((droppedPage)=>{ debug('dropped page %O', droppedPage); // nested clean up children module this.cleanUpChildren([], droppedPage); - // drop page from pageArray; - let idxOfPage = this.$pageArray.findIndex((page)=>page.src===droppedPage.src); - if (idxOfPage>-1) this.$pageArray.splice(idxOfPage, 1); - // drop module from index - let idxOfModule = this.$indexOfModule.findIndex((mdl)=>mdl.src===droppedPage.src); - if (idxOfModule>-1) this.$indexOfModule.splice(idxOfModule, 1); + if (this.$indexOfModule.has(droppedPage.src)) { + this.$indexOfModule.delete(droppedPage.src); + } - if (droppedPage.meta && !droppedPage.isAbstract) { - unlinkSync(droppedPage.meta.accOutputPath); + this.deleteFile(droppedPage); + }); + } + + deleteFile(mdl) { + if (mdl.isAbstract || !mdl.output) return; + + mdl.output.forEach((info, outputPath)=>{ + try { + if (info.reality) unlinkSync(info.reality); + } catch (e) { + logger.error(e); } }); } @@ -278,59 +270,52 @@ class Schedule { findOrAddDependency(dep, mdl) { // if a dependency is from remote, or dynamic path, or base64 format, then we ignore it. // cause we needn't process this kind of resource. - if (dep.pret.isURI || dep.pret.isDynamic || dep.pret.isBase64) return null; + if ( + dep.pret.isURI || + dep.pret.isDynamic || + dep.pret.isBase64 || + dep.pret.isPlugin + ) return null; debug('Find Dependencies started %O', simplify(dep)); // pret backup dep.pret = dep.pret || defaultPret; - let indexedModuleIdx = this.$indexOfModule.findIndex((file)=>file.src===dep.src); - debug('Find index of moduleList %s', indexedModuleIdx); let child = { ...dep, color: COLOR.INIT, isNpm: dep.pret.isNodeModule, isPlugin: dep.pret.isPlugin, - $target: dep.target, - $pret: dep.pret, - reference: [mdl], + reference: new Map([[mdl.src, mdl]]), + output: new Map([[dep.meta.outputPath, { + ...dep.meta, + package: mdl.package, + reference: mdl, + }]]), }; - if (!child.isFile) { - let content = child.content || readFile(child.src); - child.hash = crypto.createHash('md5').update(content).digest('hex'); - } - - if (indexedModuleIdx > -1) { - let indexedModule = this.$indexOfModule[indexedModuleIdx]; - let ref = child.reference; - debug('Find out module HASH is %s %O', indexedModule.hash, indexedModule); + if (this.$indexOfModule.has(dep.src)) { + let indexedModule = this.$indexOfModule.get(dep.src); - // merge from. - if (Array.isArray(indexedModule.reference)) { - indexedModule.reference.push(ref); + // merge reference, cause the module is parsed + if (indexedModule.reference instanceof Map) { + indexedModule.reference.set(mdl.src, mdl); } else { - indexedModule.reference = ref; + indexedModule.reference = new Map([[mdl.src, mdl]]); } - - if (this.mode === 'watch' && indexedModule.hash !== child.hash) { - debug('WATCH MODE and HASH is Changed'); - let newChild = {...indexedModule, ...child}; - this.$depPending.push(newChild); - - this.$indexOfModule.splice(indexedModuleIdx, 1, newChild); - child = newChild; - } else { - child = indexedModule; + // merge output + if (!indexedModule.output.has(dep.meta.outputPath)) { + indexedModule.output.set(dep.meta.outputPath, child.get(dep.meta.outputPath)); } - } else if (!child.isPlugin) { - // plugin do not resolve dependencies. - this.$depPending.push(child); - this.$indexOfModule.push(child); + + child = indexedModule; } + if (child.color !== COLOR.COMPILED) this.$depPending.push(child); + if (child.color === COLOR.INIT) this.$indexOfModule.set(child.src, child); + return child; } @@ -399,7 +384,7 @@ class Schedule { logger.error('app页面配置缺失, 请检查app.json的pages配置项'); } - let pages = this.appConfigs.pages; + let pages = this.appConfigs.pages.slice(0).map((page)=>['', page]); // multi packages process. // support both subpackages and subPackages // see: https://developers.weixin.qq.com/miniprogram/dev/framework/subpackages/basic.html @@ -407,22 +392,13 @@ class Schedule { if (pkg) { let subPages = pkg.reduce((subPkgs, pkg)=>{ - return subPkgs.concat(pkg.pages.map((subpath)=>pkg.root.replace(/\/$/, '')+'/'+subpath)); + return [pkg.root, subPkgs.concat(pkg.pages.map((subpath)=>pkg.root.replace(/\/$/, '')+'/'+subpath))]; }, []); pages = pages.concat(subPages); } - let tryPush = (page)=>{ - let idx = this.$pageArray.filter((p)=>p.src===page.src); - if (idx > -1) { - this.$pageArray.splice(idx, 1, page); - } else { - this.$pageArray.push(page); - } - }; - // pages spread - let newPages = pages.reduce((ret, page)=>{ + let newPages = pages.reduce((ret, [pkg, page])=>{ // wxa file let wxaPage = path.join(this.meta.context, page+this.meta.wxaExt); @@ -438,6 +414,7 @@ class Schedule { pagePath: page, pret: defaultPret, isAbstract: true, + package: pkg, meta: { source: wxaPage, }, @@ -459,6 +436,7 @@ class Schedule { category: 'Page', pagePath: page, pret: defaultPret, + package: pkg, meta: { source: section, outputPath, @@ -473,7 +451,7 @@ class Schedule { } }, []); - newPages.forEach((pagePoint)=>tryPush(pagePoint)); + newPages.forEach((page)=>this.$pageArray.set(page.src, page)); return newPages; } diff --git a/packages/wxa-cli/src/utils.js b/packages/wxa-cli/src/utils.js index 1d4e14ad..e0b1d3d5 100644 --- a/packages/wxa-cli/src/utils.js +++ b/packages/wxa-cli/src/utils.js @@ -3,6 +3,7 @@ import fs from 'fs'; import path from 'path'; import mkdirp from 'mkdirp'; import cache from './fs-cache'; +import crypto from 'crypto'; let current = process.cwd(); let pkg = require('../package.json'); @@ -138,3 +139,9 @@ export function applyPlugins(plugins, compiler) { export function isEmpty(n) { return n == null || n === ''; } + +export function getHash(filepath) { + let content = readFile(filepath); + + return content == null ? Date.now() : crypto.createHash('md5').update(content).digest('hex'); +}