
Dyfactor是编写和执行配置文件引导的CodeMods的平台。通过结合运行时信息和静态分析,应用程序能够以更快的速度将代码库迁移到新的API。
参见作用中的染色器
$ yarn global add dyfactor$ yarn dyfactor --help
Usage: dyfactor [options] [command]
Options:
-h, --help output usage information
Commands:
init Initializes dyfactor in your project
run Runs a plugin against code
list-plugins Lists the available plugins
help [cmd] display help for [cmd]Jscodeshift和Babel等工具是将代码从一个API迁移到另一种API的绝佳工具,因为它们依赖于静态分析。但是,应用程序的一部分完全有可能依靠运行时信息来确定执行。在其他情况下,您可能具有自定义的DSL,例如模板语言,这些语言没有与JavaScript相同的静态分析保证。迁移这种类型的代码通常需要大量的开发人员干预来弄清实际发生的事情。 Dyfactor的目标是缩短这种理解的裂缝。
在Ember的模板中,开发人员可以使用参数调用组件。他们还可以查看背景类中的本地值。组件的模板可能看起来如下。
尽管此模板是声明性的,但尚不明显的是,是否有任何MustacheExpressions (Curlies)是组件的参数,或者它们是组件类上的本地值。知道的唯一方法是去看组件的调用。在这样做时,我们发现firstName和lastName作为参数传递。
Ember Core已经意识到这是一个非常有问题的,因为应用程序的增长,因此他们允许将参数与@预先固定,而当地人可以将this. 。问题是,迁移项目中的所有模板将花费太长时间,因为它要求开发人员将这些内容分开。
这是染色器进来的地方。通过编写动态插件,我们可以通过使我们知道在运行时如何解决这些符号的方式来仪器。从那里,我们可以使用该信息手动迁移代码,或者让染色器尝试为我们进行迁移。
在高水平上,Dyfactor是跑步者和插件系统。它目前支持两种类型的插件:静态插件和动态插件。
静态插件用于执行仅需要静态分析并且仅是单个分阶段的CODEmods。这些应该被认为是围绕您会用JscodeShift或Babel编写的codemod的轻型包装器。
interface StaticPlugin {
/**
* An array containing all recursive files and directories
* under a given directory
**/
inputs : string [ ] ;
/**
* Hook called by the runner where codemod is implimented
*/
modify ( ) : void ;
} import { StaticPlugin } from 'dyfactor' ;
import * as fs from 'fs' ;
import * as recast from 'recast' ;
function filesOnly ( path ) {
return path . charAt ( path . length - 1 ) !== '/' ;
}
export default class extends StaticPlugin {
modify ( ) {
this . inputs . filter ( filesOnly ) . forEach ( input => {
let content = fs . readFileSync ( input , 'utf8' ) ;
let ast = recast . parse ( content ) ;
let add = ast . program . body [ 0 ] ;
let { builders : b } = recast . types ;
ast . program . body [ 0 ] = b . variableDeclaration ( 'var' , [
b . variableDeclarator (
add . id ,
b . functionExpression (
null , // Anonymize the function expression.
add . params ,
add . body
)
)
] ) ;
add . params . push ( add . params . shift ( ) ) ;
let output = recast . prettyPrint ( ast , { tabWidth : 2 } ) . code ;
fs . writeFileSync ( input , output ) ;
} ) ;
}
} 动态插件是两步的。第一阶段使您可以安全地仪器来收集该运行时遥测数据。然后将仪器应用程序启动使用Puppeteer来收集数据。第二阶段负责内省运行时遥测数据,并根据该数据应用CODEMODS。重要的是要注意,动态插件可以作为单个相位插件运行,只是为了生成运行时遥测并将其写入磁盘。
interface DynamicPlugin {
/**
* An array containing all recursive files and directories
* under a given directory
**/
inputs : string [ ] ;
/**
* Hook called by the runner to instrument the application
*/
instrument ( ) : void ;
/**
* Hook called by the runner to apply codemods based on the telemtry
*/
modify ( telemetry : Telemetry ) : void ;
}
interface Telemetry {
data : any ;
} import { DynamicPlugin , TelemetryBuilder } from 'dyfactor' ;
import * as fs from 'fs' ;
import { preprocess , print } from '@glimmer/syntax' ;
import { transform } from 'babel-core' ;
function filesOnly ( path ) {
return path . charAt ( path . length - 1 ) !== '/' ;
}
function instrumentCreate ( babel ) {
const { types : t } = babel ;
let ident ;
let t = new TelemetryBuilder ( ) ;
let template = babel . template ( `
IDENT.reopenClass({
create(injections) {
let instance = this._super(injections);
${ t . preamble ( ) }
${ t . conditionallyAdd (
'instance._debugContainerKey' ,
( ) => {
return `Object.keys(injections.attrs).forEach((arg) => {
if (! ${ t . path ( 'instance._debugContainerKey' ) } .contains(arg)) {
${ t . path ( 'instance._debugContainerKey' ) } .push(arg);
}
});` ;
} ,
( ) => {
return ` ${ t . path ( 'instance._debugContainerKey' ) } = Object.keys(injections.attrs);` ;
}
) }
return instance;
}
});
` ) ;
return {
name : 'instrument-create' ,
visitor : {
Program : {
enter ( p ) {
ident = p . scope . generateUidIdentifier ( 'refactor' ) ;
} ,
exit ( p ) {
let body = p . node . body ;
let ast = template ( { IDENT : ident } ) ;
body . push ( ast , t . exportDefaultDeclaration ( ident ) ) ;
}
} ,
ExportDefaultDeclaration ( p ) {
let declaration = p . node . declaration ;
let declarator = t . variableDeclarator ( ident , declaration ) ;
let varDecl = t . variableDeclaration ( 'const' , [ declarator ] ) ;
p . replaceWith ( varDecl ) ;
}
}
} ;
}
export default class extends DynamicPlugin {
instrument ( ) {
this . inputs . filter ( filesOnly ) . forEach ( input => {
let code = fs . readFileSync ( input , 'utf8' ) ;
let content = transform ( code , {
plugins : [ instrumentCreate ]
} ) ;
fs . writeFileSync ( input , content . code ) ;
} ) ;
}
modify ( telemetry ) {
telemetry . data . forEach ( components => {
Object . keys ( components ) . forEach ( component => {
let templatePath = this . templateFor ( component ) ;
let template = fs . readFileSync ( templatePath , 'utf8' ) ;
let ast = preprocess ( template , {
plugins : {
ast : [ toArgs ( components [ component ] ) ]
}
} ) ;
fs . writeFileSync ( templatePath , print ( ast ) ) ;
} ) ;
} ) ;
}
templateFor ( fullName : string ) {
let [ , name ] = fullName . split ( ':' ) ;
return this . inputs . find ( input => input . includes ( `templates/components/ ${ name } ` ) ) ;
}
}