
Dyfaktor ist eine Plattform zum Schreiben und Ausführen von profilgesteuerten Codemods. Durch die Kombination von Laufzeitinformationen und statische Analysen sind Anwendungen in der Lage, Codebasen in viel schnellerem Tempo auf neue APIs zu migrieren.
Siehe Dyfaktor in Aktion
$ 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]Tools wie Jscodeshift und Babel sind hervorragende Werkzeuge für die Migration von Code von einer API zur anderen, da sie sich auf statische Analysen verlassen. Es ist jedoch durchaus möglich, dass Teile einer Anwendung auf Laufzeitinformationen angewiesen sind, um die Ausführung zu bestimmen. In anderen Fällen haben Sie möglicherweise benutzerdefinierte DSLs wie Vorlagensprachen, die nicht die gleichen statischen Analysegarantien haben wie JavaScript. Die Migration dieser Art von Code erfordert in der Regel eine Menge Entwicklerinterventionen, um herauszufinden, was tatsächlich passiert. Das Ziel des Dyfaktors ist es, diese Spalt des Verständnisses zu verkürzen.
In Embers Vorlagenschicht können Entwickler Komponenten mit Argumenten aufrufen. Sie können auch lokale Werte in der Backing -Klasse betrachten. Eine Vorlage für eine Komponente kann wie die folgenden aussehen.
Während diese Vorlage deklarativ ist, ist es nicht offensichtlich, ob eine der MustacheExpressions (Locken) Argumente an die Komponente war oder ob sie lokale Werte in der Komponentenklasse sind. Der einzige Weg zu wissen besteht darin, sich die Aufruf der Komponente anzusehen. Dabei finden wir, dass firstName und lastName als Argumente übergeben werden.
Der Ember-Kern hat erkannt, dass dies extrem problematisch ist, wenn eine Anwendung wächst. Daher haben sie es ermöglicht, Argumente mit @ und den Einheimischen vorzuvermachten this. . Das Problem ist, dass die Migration aller Vorlagen in einem Projekt zu lange dauern würde, da die Entwickler diese Dinge trennen müssen.
Hier kommt der Dyfaktor ins Spiel. Durch das Schreiben eines dynamischen Plugins können wir die Anwendung so instrumentieren, dass wir wissen, wie diese Symbole zur Laufzeit aufgelöst werden. Von dort aus können wir diese Informationen verwenden, um den Code manuell zu migrieren oder Dyfaktor zu versuchen, die Migration für uns durchzuführen.
Dyfactor ist ein Läufer und ein Plugin -System. Derzeit unterstützt es zwei Arten von Plugins: statische Plugins und dynamische Plugins.
Ein statisches Plugin soll verwendet werden, um Codemoden durchzuführen, die nur eine statische Analyse erfordern, und nur einzeln Phasen. Diese sollten als leichte Wrapper um einen Codemod angesehen werden, den Sie mit JSCodeshift oder Babel schreiben würden.
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 ) ;
} ) ;
}
} Ein dynamisches Plugin ist zweiphasen. In der ersten Phase können Sie eine Anwendung sicher instrumentieren, um diese Telemetriedaten der Laufzeit zu erfassen. Die instrumentierte Anwendung wird dann mit Puppenspielern gebootet, um die Daten zu sammeln. Die zweite Phase ist verantwortlich für die Einführung der Telemetriedaten der Laufzeit und die Anwendung von Codemods basierend auf diesen Daten. Es ist wichtig zu beachten, dass dynamische Plugins als Single -Phased -Plugins ausgeführt werden können, um die Laufzeit -Telemetrie zu erstellen und auf die Festplatte zu schreiben.
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 } ` ) ) ;
}
}