
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 } ` ) ) ;
}
}