123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173 |
- 'use strict';
- /**
- * @license Angular v19.2.4
- * (c) 2010-2025 Google LLC. https://angular.io/
- * License: MIT
- */
- 'use strict';
- var schematics = require('@angular-devkit/schematics');
- var p = require('path');
- var compiler_host = require('./compiler_host-DzM2hemp.js');
- var ts = require('typescript');
- var ng_decorators = require('./ng_decorators-DznZ5jMl.js');
- var imports = require('./imports-CIX-JgAN.js');
- var nodes = require('./nodes-B16H9JUd.js');
- var leading_space = require('./leading_space-D9nQ8UQC.js');
- require('./checker-DP-zos5Q.js');
- require('os');
- require('fs');
- require('module');
- require('url');
- /*!
- * @license
- * Copyright Google LLC All Rights Reserved.
- *
- * Use of this source code is governed by an MIT-style license that can be
- * found in the LICENSE file at https://angular.dev/license
- */
- /** Names of decorators that enable DI on a class declaration. */
- const DECORATORS_SUPPORTING_DI = new Set([
- 'Component',
- 'Directive',
- 'Pipe',
- 'NgModule',
- 'Injectable',
- ]);
- /** Names of symbols used for DI on parameters. */
- const DI_PARAM_SYMBOLS = new Set([
- 'Inject',
- 'Attribute',
- 'Optional',
- 'SkipSelf',
- 'Self',
- 'Host',
- 'forwardRef',
- ]);
- /** Kinds of nodes which aren't injectable when set as a type of a parameter. */
- const UNINJECTABLE_TYPE_KINDS = new Set([
- ts.SyntaxKind.TrueKeyword,
- ts.SyntaxKind.FalseKeyword,
- ts.SyntaxKind.NumberKeyword,
- ts.SyntaxKind.StringKeyword,
- ts.SyntaxKind.NullKeyword,
- ts.SyntaxKind.VoidKeyword,
- ]);
- /**
- * Finds the necessary information for the `inject` migration in a file.
- * @param sourceFile File which to analyze.
- * @param localTypeChecker Type checker scoped to the specific file.
- */
- function analyzeFile(sourceFile, localTypeChecker, options) {
- const coreSpecifiers = imports.getNamedImports(sourceFile, '@angular/core');
- // Exit early if there are no Angular imports.
- if (coreSpecifiers === null || coreSpecifiers.elements.length === 0) {
- return null;
- }
- const classes = [];
- const nonDecoratorReferences = {};
- const importsToSpecifiers = coreSpecifiers.elements.reduce((map, specifier) => {
- const symbolName = (specifier.propertyName || specifier.name).text;
- if (DI_PARAM_SYMBOLS.has(symbolName)) {
- map.set(symbolName, specifier);
- }
- return map;
- }, new Map());
- sourceFile.forEachChild(function walk(node) {
- // Skip import declarations since they can throw off the identifier
- // could below and we don't care about them in this migration.
- if (ts.isImportDeclaration(node)) {
- return;
- }
- if (ts.isParameter(node)) {
- const closestConstructor = nodes.closestNode(node, ts.isConstructorDeclaration);
- // Visiting the same parameters that we're about to remove can throw off the reference
- // counting logic below. If we run into an initializer, we always visit its initializer
- // and optionally visit the modifiers/decorators if it's not due to be deleted. Note that
- // here we technically aren't dealing with the the full list of classes, but the parent class
- // will have been visited by the time we reach the parameters.
- if (node.initializer) {
- walk(node.initializer);
- }
- if (closestConstructor === null ||
- // This is meant to avoid the case where this is a
- // parameter inside a function placed in a constructor.
- !closestConstructor.parameters.includes(node) ||
- !classes.some((c) => c.constructor === closestConstructor)) {
- node.modifiers?.forEach(walk);
- }
- return;
- }
- if (ts.isIdentifier(node) && importsToSpecifiers.size > 0) {
- let symbol;
- for (const [name, specifier] of importsToSpecifiers) {
- const localName = (specifier.propertyName || specifier.name).text;
- // Quick exit if the two symbols don't match up.
- if (localName === node.text) {
- if (!symbol) {
- symbol = localTypeChecker.getSymbolAtLocation(node);
- // If the symbol couldn't be resolved the first time, it won't be resolved the next
- // time either. Stop the loop since we won't be able to get an accurate result.
- if (!symbol || !symbol.declarations) {
- break;
- }
- else if (symbol.declarations.some((decl) => decl === specifier)) {
- nonDecoratorReferences[name] = (nonDecoratorReferences[name] || 0) + 1;
- }
- }
- }
- }
- }
- else if (ts.isClassDeclaration(node)) {
- const decorators = ng_decorators.getAngularDecorators(localTypeChecker, ts.getDecorators(node) || []);
- const isAbstract = !!node.modifiers?.some((m) => m.kind === ts.SyntaxKind.AbstractKeyword);
- const supportsDI = decorators.some((dec) => DECORATORS_SUPPORTING_DI.has(dec.name));
- const constructorNode = node.members.find((member) => ts.isConstructorDeclaration(member) &&
- member.body != null &&
- member.parameters.length > 0);
- // Basic check to determine if all parameters are injectable. This isn't exhaustive, but it
- // should catch the majority of cases. An exhaustive check would require a full type checker
- // which we don't have in this migration.
- const allParamsInjectable = !!constructorNode?.parameters.every((param) => {
- if (!param.type || !UNINJECTABLE_TYPE_KINDS.has(param.type.kind)) {
- return true;
- }
- return ng_decorators.getAngularDecorators(localTypeChecker, ts.getDecorators(param) || []).some((dec) => dec.name === 'Inject' || dec.name === 'Attribute');
- });
- // Don't migrate abstract classes by default, because
- // their parameters aren't guaranteed to be injectable.
- if (supportsDI &&
- constructorNode &&
- allParamsInjectable &&
- (!isAbstract || options.migrateAbstractClasses)) {
- classes.push({
- node,
- constructor: constructorNode,
- superCall: node.heritageClauses ? findSuperCall(constructorNode) : null,
- });
- }
- }
- node.forEachChild(walk);
- });
- return { classes, nonDecoratorReferences };
- }
- /**
- * Returns the parameters of a function that aren't used within its body.
- * @param declaration Function in which to search for unused parameters.
- * @param localTypeChecker Type checker scoped to the file in which the function was declared.
- * @param removedStatements Statements that were already removed from the constructor.
- */
- function getConstructorUnusedParameters(declaration, localTypeChecker, removedStatements) {
- const accessedTopLevelParameters = new Set();
- const topLevelParameters = new Set();
- const topLevelParameterNames = new Set();
- const unusedParams = new Set();
- // Prepare the parameters for quicker checks further down.
- for (const param of declaration.parameters) {
- if (ts.isIdentifier(param.name)) {
- topLevelParameters.add(param);
- topLevelParameterNames.add(param.name.text);
- }
- }
- if (!declaration.body) {
- return topLevelParameters;
- }
- const analyze = (node) => {
- // Don't descend into statements that were removed already.
- if (ts.isStatement(node) && removedStatements.has(node)) {
- return;
- }
- if (!ts.isIdentifier(node) || !topLevelParameterNames.has(node.text)) {
- node.forEachChild(analyze);
- return;
- }
- // Don't consider `this.<name>` accesses as being references to
- // parameters since they'll be moved to property declarations.
- if (isAccessedViaThis(node)) {
- return;
- }
- localTypeChecker.getSymbolAtLocation(node)?.declarations?.forEach((decl) => {
- if (ts.isParameter(decl) && topLevelParameters.has(decl)) {
- accessedTopLevelParameters.add(decl);
- }
- if (ts.isShorthandPropertyAssignment(decl)) {
- const symbol = localTypeChecker.getShorthandAssignmentValueSymbol(decl);
- if (symbol && symbol.valueDeclaration && ts.isParameter(symbol.valueDeclaration)) {
- accessedTopLevelParameters.add(symbol.valueDeclaration);
- }
- }
- });
- };
- declaration.parameters.forEach((param) => {
- if (param.initializer) {
- analyze(param.initializer);
- }
- });
- declaration.body.forEachChild(analyze);
- for (const param of topLevelParameters) {
- if (!accessedTopLevelParameters.has(param)) {
- unusedParams.add(param);
- }
- }
- return unusedParams;
- }
- /**
- * Determines which parameters of a function declaration are used within its `super` call.
- * @param declaration Function whose parameters to search for.
- * @param superCall `super()` call within the function.
- * @param localTypeChecker Type checker scoped to the file in which the function is declared.
- */
- function getSuperParameters(declaration, superCall, localTypeChecker) {
- const usedParams = new Set();
- const topLevelParameters = new Set();
- const topLevelParameterNames = new Set();
- // Prepare the parameters for quicker checks further down.
- for (const param of declaration.parameters) {
- if (ts.isIdentifier(param.name)) {
- topLevelParameters.add(param);
- topLevelParameterNames.add(param.name.text);
- }
- }
- superCall.forEachChild(function walk(node) {
- if (ts.isIdentifier(node) && topLevelParameterNames.has(node.text)) {
- localTypeChecker.getSymbolAtLocation(node)?.declarations?.forEach((decl) => {
- if (ts.isParameter(decl) && topLevelParameters.has(decl)) {
- usedParams.add(decl);
- }
- });
- }
- else {
- node.forEachChild(walk);
- }
- });
- return usedParams;
- }
- /**
- * Determines if a specific parameter has references to other parameters.
- * @param param Parameter to check.
- * @param allParameters All parameters of the containing function.
- * @param localTypeChecker Type checker scoped to the current file.
- */
- function parameterReferencesOtherParameters(param, allParameters, localTypeChecker) {
- // A parameter can only reference other parameters through its initializer.
- if (!param.initializer || allParameters.length < 2) {
- return false;
- }
- const paramNames = new Set();
- for (const current of allParameters) {
- if (current !== param && ts.isIdentifier(current.name)) {
- paramNames.add(current.name.text);
- }
- }
- let result = false;
- const analyze = (node) => {
- if (ts.isIdentifier(node) && paramNames.has(node.text) && !isAccessedViaThis(node)) {
- const symbol = localTypeChecker.getSymbolAtLocation(node);
- const referencesOtherParam = symbol?.declarations?.some((decl) => {
- return allParameters.includes(decl);
- });
- if (referencesOtherParam) {
- result = true;
- }
- }
- if (!result) {
- node.forEachChild(analyze);
- }
- };
- analyze(param.initializer);
- return result;
- }
- /** Checks whether a parameter node declares a property on its class. */
- function parameterDeclaresProperty(node) {
- return !!node.modifiers?.some(({ kind }) => kind === ts.SyntaxKind.PublicKeyword ||
- kind === ts.SyntaxKind.PrivateKeyword ||
- kind === ts.SyntaxKind.ProtectedKeyword ||
- kind === ts.SyntaxKind.ReadonlyKeyword);
- }
- /** Checks whether a type node is nullable. */
- function isNullableType(node) {
- // Apparently `foo: null` is `Parameter<TypeNode<NullKeyword>>`,
- // while `foo: undefined` is `Parameter<UndefinedKeyword>`...
- if (node.kind === ts.SyntaxKind.UndefinedKeyword || node.kind === ts.SyntaxKind.VoidKeyword) {
- return true;
- }
- if (ts.isLiteralTypeNode(node)) {
- return node.literal.kind === ts.SyntaxKind.NullKeyword;
- }
- if (ts.isUnionTypeNode(node)) {
- return node.types.some(isNullableType);
- }
- return false;
- }
- /** Checks whether a type node has generic arguments. */
- function hasGenerics(node) {
- if (ts.isTypeReferenceNode(node)) {
- return node.typeArguments != null && node.typeArguments.length > 0;
- }
- if (ts.isUnionTypeNode(node)) {
- return node.types.some(hasGenerics);
- }
- return false;
- }
- /** Checks whether an identifier is accessed through `this`, e.g. `this.<some identifier>`. */
- function isAccessedViaThis(node) {
- return (ts.isPropertyAccessExpression(node.parent) &&
- node.parent.expression.kind === ts.SyntaxKind.ThisKeyword &&
- node.parent.name === node);
- }
- /** Finds a `super` call inside of a specific node. */
- function findSuperCall(root) {
- let result = null;
- root.forEachChild(function find(node) {
- if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.SuperKeyword) {
- result = node;
- }
- else if (result === null) {
- node.forEachChild(find);
- }
- });
- return result;
- }
- /*!
- * @license
- * Copyright Google LLC All Rights Reserved.
- *
- * Use of this source code is governed by an MIT-style license that can be
- * found in the LICENSE file at https://angular.dev/license
- */
- /**
- * Finds class property declarations without initializers whose constructor-based initialization
- * can be inlined into the declaration spot after migrating to `inject`. For example:
- *
- * ```ts
- * private foo: number;
- *
- * constructor(private service: MyService) {
- * this.foo = this.service.getFoo();
- * }
- * ```
- *
- * The initializer of `foo` can be inlined, because `service` will be initialized
- * before it after the `inject` migration has finished running.
- *
- * @param node Class declaration that is being migrated.
- * @param constructor Constructor declaration of the class being migrated.
- * @param localTypeChecker Type checker scoped to the current file.
- */
- function findUninitializedPropertiesToCombine(node, constructor, localTypeChecker) {
- let toCombine = null;
- let toHoist = [];
- const membersToDeclarations = new Map();
- for (const member of node.members) {
- if (ts.isPropertyDeclaration(member) &&
- !member.initializer &&
- !ts.isComputedPropertyName(member.name)) {
- membersToDeclarations.set(member.name.text, member);
- }
- }
- if (membersToDeclarations.size === 0) {
- return null;
- }
- const memberInitializers = getMemberInitializers(constructor);
- if (memberInitializers === null) {
- return null;
- }
- for (const [name, decl] of membersToDeclarations.entries()) {
- if (memberInitializers.has(name)) {
- const initializer = memberInitializers.get(name);
- if (!hasLocalReferences(initializer, constructor, localTypeChecker)) {
- toCombine ??= [];
- toCombine.push({ declaration: membersToDeclarations.get(name), initializer });
- }
- }
- else {
- // Mark members that have no initializers and can't be combined to be hoisted above the
- // injected members. This is either a no-op or it allows us to avoid some patterns internally
- // like the following:
- // ```
- // class Foo {
- // publicFoo: Foo;
- // private privateFoo: Foo;
- //
- // constructor() {
- // this.initializePrivateFooSomehow();
- // this.publicFoo = this.privateFoo;
- // }
- // }
- // ```
- toHoist.push(decl);
- }
- }
- // If no members need to be combined, none need to be hoisted either.
- return toCombine === null ? null : { toCombine, toHoist };
- }
- /**
- * In some cases properties may be declared out of order, but initialized in the correct order.
- * The internal-specific migration will combine such properties which will result in a compilation
- * error, for example:
- *
- * ```ts
- * class MyClass {
- * foo: Foo;
- * bar: Bar;
- *
- * constructor(bar: Bar) {
- * this.bar = bar;
- * this.foo = this.bar.getFoo();
- * }
- * }
- * ```
- *
- * Will become:
- *
- * ```ts
- * class MyClass {
- * foo: Foo = this.bar.getFoo();
- * bar: Bar = inject(Bar);
- * }
- * ```
- *
- * This function determines if cases like this can be saved by reordering the properties so their
- * declaration order matches the order in which they're initialized.
- *
- * @param toCombine Properties that are candidates to be combined.
- * @param constructor
- */
- function shouldCombineInInitializationOrder(toCombine, constructor) {
- let combinedMemberReferenceCount = 0;
- let otherMemberReferenceCount = 0;
- const injectedMemberNames = new Set();
- const combinedMemberNames = new Set();
- // Collect the name of constructor parameters that declare new properties.
- // These can be ignored since they'll be hoisted above other properties.
- constructor.parameters.forEach((param) => {
- if (parameterDeclaresProperty(param) && ts.isIdentifier(param.name)) {
- injectedMemberNames.add(param.name.text);
- }
- });
- // Collect the names of the properties being combined. We should only reorder
- // the properties if at least one of them refers to another one.
- toCombine.forEach(({ declaration: { name } }) => {
- if (ts.isStringLiteralLike(name) || ts.isIdentifier(name)) {
- combinedMemberNames.add(name.text);
- }
- });
- // Visit all the initializers and check all the property reads in the form of `this.<name>`.
- // Skip over the ones referring to injected parameters since they're going to be hoisted.
- const walkInitializer = (node) => {
- if (ts.isPropertyAccessExpression(node) && node.expression.kind === ts.SyntaxKind.ThisKeyword) {
- if (combinedMemberNames.has(node.name.text)) {
- combinedMemberReferenceCount++;
- }
- else if (!injectedMemberNames.has(node.name.text)) {
- otherMemberReferenceCount++;
- }
- }
- node.forEachChild(walkInitializer);
- };
- toCombine.forEach((candidate) => walkInitializer(candidate.initializer));
- // If at the end there is at least one reference between a combined member and another,
- // and there are no references to any other class members, we can safely reorder the
- // properties based on how they were initialized.
- return combinedMemberReferenceCount > 0 && otherMemberReferenceCount === 0;
- }
- /**
- * Finds the expressions from the constructor that initialize class members, for example:
- *
- * ```ts
- * private foo: number;
- *
- * constructor() {
- * this.foo = 123;
- * }
- * ```
- *
- * @param constructor Constructor declaration being analyzed.
- */
- function getMemberInitializers(constructor) {
- let memberInitializers = null;
- if (!constructor.body) {
- return memberInitializers;
- }
- // Only look at top-level constructor statements.
- for (const node of constructor.body.statements) {
- // Only look for statements in the form of `this.<name> = <expr>;` or `this[<name>] = <expr>;`.
- if (!ts.isExpressionStatement(node) ||
- !ts.isBinaryExpression(node.expression) ||
- node.expression.operatorToken.kind !== ts.SyntaxKind.EqualsToken ||
- (!ts.isPropertyAccessExpression(node.expression.left) &&
- !ts.isElementAccessExpression(node.expression.left)) ||
- node.expression.left.expression.kind !== ts.SyntaxKind.ThisKeyword) {
- continue;
- }
- let name;
- if (ts.isPropertyAccessExpression(node.expression.left)) {
- name = node.expression.left.name.text;
- }
- else if (ts.isElementAccessExpression(node.expression.left)) {
- name = ts.isStringLiteralLike(node.expression.left.argumentExpression)
- ? node.expression.left.argumentExpression.text
- : undefined;
- }
- // If the member is initialized multiple times, take the first one.
- if (name && (!memberInitializers || !memberInitializers.has(name))) {
- memberInitializers = memberInitializers || new Map();
- memberInitializers.set(name, node.expression.right);
- }
- }
- return memberInitializers;
- }
- /**
- * Determines if a node has references to local symbols defined in the constructor.
- * @param root Expression to check for local references.
- * @param constructor Constructor within which the expression is used.
- * @param localTypeChecker Type checker scoped to the current file.
- */
- function hasLocalReferences(root, constructor, localTypeChecker) {
- const sourceFile = root.getSourceFile();
- let hasLocalRefs = false;
- const walk = (node) => {
- // Stop searching if we know that it has local references.
- if (hasLocalRefs) {
- return;
- }
- // Skip identifiers that are accessed via `this` since they're accessing class members
- // that aren't local to the constructor. This is here primarily to catch cases like this
- // where `foo` is defined inside the constructor, but is a class member:
- // ```
- // constructor(private foo: Foo) {
- // this.bar = this.foo.getFoo();
- // }
- // ```
- if (ts.isIdentifier(node) && !isAccessedViaThis(node)) {
- const declarations = localTypeChecker.getSymbolAtLocation(node)?.declarations;
- const isReferencingLocalSymbol = declarations?.some((decl) =>
- // The source file check is a bit redundant since the type checker
- // is local to the file, but it's inexpensive and it can prevent
- // bugs in the future if we decide to use a full type checker.
- decl.getSourceFile() === sourceFile &&
- decl.getStart() >= constructor.getStart() &&
- decl.getEnd() <= constructor.getEnd() &&
- !isInsideInlineFunction(decl, constructor));
- if (isReferencingLocalSymbol) {
- hasLocalRefs = true;
- }
- }
- if (!hasLocalRefs) {
- node.forEachChild(walk);
- }
- };
- walk(root);
- return hasLocalRefs;
- }
- /**
- * Determines if a node is defined inside of an inline function.
- * @param startNode Node from which to start checking for inline functions.
- * @param boundary Node at which to stop searching.
- */
- function isInsideInlineFunction(startNode, boundary) {
- let current = startNode;
- while (current) {
- if (current === boundary) {
- return false;
- }
- if (ts.isFunctionDeclaration(current) ||
- ts.isFunctionExpression(current) ||
- ts.isArrowFunction(current)) {
- return true;
- }
- current = current.parent;
- }
- return false;
- }
- /**
- * Placeholder used to represent expressions inside the AST.
- * Includes Unicode characters to reduce the chance of collisions.
- */
- const PLACEHOLDER = 'ɵɵngGeneratePlaceholderɵɵ';
- /**
- * Migrates all of the classes in a `SourceFile` away from constructor injection.
- * @param sourceFile File to be migrated.
- * @param options Options that configure the migration.
- */
- function migrateFile(sourceFile, options) {
- // Note: even though externally we have access to the full program with a proper type
- // checker, we create a new one that is local to the file for a couple of reasons:
- // 1. Not having to depend on a program makes running the migration internally faster and easier.
- // 2. All the necessary information for this migration is local so using a file-specific type
- // checker should speed up the lookups.
- const localTypeChecker = getLocalTypeChecker(sourceFile);
- const analysis = analyzeFile(sourceFile, localTypeChecker, options);
- if (analysis === null || analysis.classes.length === 0) {
- return [];
- }
- const printer = ts.createPrinter();
- const tracker = new compiler_host.ChangeTracker(printer);
- analysis.classes.forEach(({ node, constructor, superCall }) => {
- const memberIndentation = leading_space.getLeadingLineWhitespaceOfNode(node.members[0]);
- const prependToClass = [];
- const afterInjectCalls = [];
- const removedStatements = new Set();
- const removedMembers = new Set();
- if (options._internalCombineMemberInitializers) {
- applyInternalOnlyChanges(node, constructor, localTypeChecker, tracker, printer, removedStatements, removedMembers, prependToClass, afterInjectCalls, memberIndentation);
- }
- migrateClass(node, constructor, superCall, options, memberIndentation, prependToClass, afterInjectCalls, removedStatements, removedMembers, localTypeChecker, printer, tracker);
- });
- DI_PARAM_SYMBOLS.forEach((name) => {
- // Both zero and undefined are fine here.
- if (!analysis.nonDecoratorReferences[name]) {
- tracker.removeImport(sourceFile, name, '@angular/core');
- }
- });
- return tracker.recordChanges().get(sourceFile) || [];
- }
- /**
- * Migrates a class away from constructor injection.
- * @param node Class to be migrated.
- * @param constructor Reference to the class' constructor node.
- * @param superCall Reference to the constructor's `super()` call, if any.
- * @param options Options used to configure the migration.
- * @param memberIndentation Indentation string of the members of the class.
- * @param prependToClass Text that should be prepended to the class.
- * @param afterInjectCalls Text that will be inserted after the newly-added `inject` calls.
- * @param removedStatements Statements that have been removed from the constructor already.
- * @param removedMembers Class members that have been removed by the migration.
- * @param localTypeChecker Type checker set up for the specific file.
- * @param printer Printer used to output AST nodes as strings.
- * @param tracker Object keeping track of the changes made to the file.
- */
- function migrateClass(node, constructor, superCall, options, memberIndentation, prependToClass, afterInjectCalls, removedStatements, removedMembers, localTypeChecker, printer, tracker) {
- const sourceFile = node.getSourceFile();
- const unusedParameters = getConstructorUnusedParameters(constructor, localTypeChecker, removedStatements);
- const superParameters = superCall
- ? getSuperParameters(constructor, superCall, localTypeChecker)
- : null;
- const removedStatementCount = removedStatements.size;
- const firstConstructorStatement = constructor.body?.statements.find((statement) => !removedStatements.has(statement));
- const innerReference = superCall || firstConstructorStatement || constructor;
- const innerIndentation = leading_space.getLeadingLineWhitespaceOfNode(innerReference);
- const prependToConstructor = [];
- const afterSuper = [];
- for (const param of constructor.parameters) {
- const usedInSuper = superParameters !== null && superParameters.has(param);
- const usedInConstructor = !unusedParameters.has(param);
- const usesOtherParams = parameterReferencesOtherParameters(param, constructor.parameters, localTypeChecker);
- migrateParameter(param, options, localTypeChecker, printer, tracker, superCall, usedInSuper, usedInConstructor, usesOtherParams, memberIndentation, innerIndentation, prependToConstructor, prependToClass, afterSuper);
- }
- // Delete all of the constructor overloads since below we're either going to
- // remove the implementation, or we're going to delete all of the parameters.
- for (const member of node.members) {
- if (ts.isConstructorDeclaration(member) && member !== constructor) {
- removedMembers.add(member);
- tracker.removeNode(member, true);
- }
- }
- if (canRemoveConstructor(options, constructor, removedStatementCount, prependToConstructor, superCall)) {
- // Drop the constructor if it was empty.
- removedMembers.add(constructor);
- tracker.removeNode(constructor, true);
- }
- else {
- // If the constructor contains any statements, only remove the parameters.
- // We always do this no matter what is passed into `backwardsCompatibleConstructors`.
- stripConstructorParameters(constructor, tracker);
- if (prependToConstructor.length > 0) {
- if (firstConstructorStatement ||
- (innerReference !== constructor &&
- innerReference.getStart() >= constructor.getStart() &&
- innerReference.getEnd() <= constructor.getEnd())) {
- tracker.insertText(sourceFile, (firstConstructorStatement || innerReference).getFullStart(), `\n${prependToConstructor.join('\n')}\n`);
- }
- else {
- tracker.insertText(sourceFile, constructor.body.getStart() + 1, `\n${prependToConstructor.map((p) => innerIndentation + p).join('\n')}\n${innerIndentation}`);
- }
- }
- }
- if (afterSuper.length > 0 && superCall !== null) {
- // Note that if we can, we should insert before the next statement after the `super` call,
- // rather than after the end of it. Otherwise the string buffering implementation may drop
- // the text if the statement after the `super` call is being deleted. This appears to be because
- // the full start of the next statement appears to always be the end of the `super` call plus 1.
- const nextStatement = getNextPreservedStatement(superCall, removedStatements);
- tracker.insertText(sourceFile, nextStatement ? nextStatement.getFullStart() : constructor.getEnd() - 1, `\n${afterSuper.join('\n')}\n` + (nextStatement ? '' : memberIndentation));
- }
- // Need to resolve this once all constructor signatures have been removed.
- const memberReference = node.members.find((m) => !removedMembers.has(m)) || node.members[0];
- // If `backwardsCompatibleConstructors` is enabled, we maintain
- // backwards compatibility by adding a catch-all signature.
- if (options.backwardsCompatibleConstructors) {
- const extraSignature = `\n${memberIndentation}/** Inserted by Angular inject() migration for backwards compatibility */\n` +
- `${memberIndentation}constructor(...args: unknown[]);`;
- // The new signature always has to be right before the constructor implementation.
- if (memberReference === constructor) {
- prependToClass.push(extraSignature);
- }
- else {
- tracker.insertText(sourceFile, constructor.getFullStart(), '\n' + extraSignature);
- }
- }
- // Push the block of code that should appear after the `inject`
- // calls now once all the members have been generated.
- prependToClass.push(...afterInjectCalls);
- if (prependToClass.length > 0) {
- if (removedMembers.size === node.members.length) {
- tracker.insertText(sourceFile,
- // If all members were deleted, insert after the last one.
- // This allows us to preserve the indentation.
- node.members.length > 0
- ? node.members[node.members.length - 1].getEnd() + 1
- : node.getEnd() - 1, `${prependToClass.join('\n')}\n`);
- }
- else {
- // Insert the new properties after the first member that hasn't been deleted.
- tracker.insertText(sourceFile, memberReference.getFullStart(), `\n${prependToClass.join('\n')}\n`);
- }
- }
- }
- /**
- * Migrates a single parameter to `inject()` DI.
- * @param node Parameter to be migrated.
- * @param options Options used to configure the migration.
- * @param localTypeChecker Type checker set up for the specific file.
- * @param printer Printer used to output AST nodes as strings.
- * @param tracker Object keeping track of the changes made to the file.
- * @param superCall Call to `super()` from the class' constructor.
- * @param usedInSuper Whether the parameter is referenced inside of `super`.
- * @param usedInConstructor Whether the parameter is referenced inside the body of the constructor.
- * @param memberIndentation Indentation string to use when inserting new class members.
- * @param innerIndentation Indentation string to use when inserting new constructor statements.
- * @param prependToConstructor Statements to be prepended to the constructor.
- * @param propsToAdd Properties to be added to the class.
- * @param afterSuper Statements to be added after the `super` call.
- */
- function migrateParameter(node, options, localTypeChecker, printer, tracker, superCall, usedInSuper, usedInConstructor, usesOtherParams, memberIndentation, innerIndentation, prependToConstructor, propsToAdd, afterSuper) {
- if (!ts.isIdentifier(node.name)) {
- return;
- }
- const name = node.name.text;
- const replacementCall = createInjectReplacementCall(node, options, localTypeChecker, printer, tracker);
- const declaresProp = parameterDeclaresProperty(node);
- // If the parameter declares a property, we need to declare it (e.g. `private foo: Foo`).
- if (declaresProp) {
- // We can't initialize the property if it's referenced within a `super` call or it references
- // other parameters. See the logic further below for the initialization.
- const canInitialize = !usedInSuper && !usesOtherParams;
- const prop = ts.factory.createPropertyDeclaration(cloneModifiers(node.modifiers?.filter((modifier) => {
- // Strip out the DI decorators, as well as `public` which is redundant.
- return !ts.isDecorator(modifier) && modifier.kind !== ts.SyntaxKind.PublicKeyword;
- })), name,
- // Don't add the question token to private properties since it won't affect interface implementation.
- node.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.PrivateKeyword)
- ? undefined
- : node.questionToken, canInitialize ? undefined : node.type, canInitialize ? ts.factory.createIdentifier(PLACEHOLDER) : undefined);
- propsToAdd.push(memberIndentation +
- replaceNodePlaceholder(node.getSourceFile(), prop, replacementCall, printer));
- }
- // If the parameter is referenced within the constructor, we need to declare it as a variable.
- if (usedInConstructor) {
- if (usedInSuper) {
- // Usages of `this` aren't allowed before `super` calls so we need to
- // create a variable which calls `inject()` directly instead...
- prependToConstructor.push(`${innerIndentation}const ${name} = ${replacementCall};`);
- // ...then we can initialize the property after the `super` call.
- if (declaresProp) {
- afterSuper.push(`${innerIndentation}this.${name} = ${name};`);
- }
- }
- else if (declaresProp) {
- // If the parameter declares a property (`private foo: foo`) and is used inside the class
- // at the same time, we need to ensure that it's initialized to the value from the variable
- // and that we only reference `this` after the `super` call.
- const initializer = `${innerIndentation}const ${name} = this.${name};`;
- if (superCall === null) {
- prependToConstructor.push(initializer);
- }
- else {
- afterSuper.push(initializer);
- }
- }
- else {
- // If the parameter is only referenced in the constructor, we
- // don't need to declare any new properties.
- prependToConstructor.push(`${innerIndentation}const ${name} = ${replacementCall};`);
- }
- }
- else if (usesOtherParams && declaresProp) {
- const toAdd = `${innerIndentation}this.${name} = ${replacementCall};`;
- if (superCall === null) {
- prependToConstructor.push(toAdd);
- }
- else {
- afterSuper.push(toAdd);
- }
- }
- }
- /**
- * Creates a replacement `inject` call from a function parameter.
- * @param param Parameter for which to generate the `inject` call.
- * @param options Options used to configure the migration.
- * @param localTypeChecker Type checker set up for the specific file.
- * @param printer Printer used to output AST nodes as strings.
- * @param tracker Object keeping track of the changes made to the file.
- */
- function createInjectReplacementCall(param, options, localTypeChecker, printer, tracker) {
- const moduleName = '@angular/core';
- const sourceFile = param.getSourceFile();
- const decorators = ng_decorators.getAngularDecorators(localTypeChecker, ts.getDecorators(param) || []);
- const literalProps = [];
- const type = param.type;
- let injectedType = '';
- let typeArguments = type && hasGenerics(type) ? [type] : undefined;
- let hasOptionalDecorator = false;
- if (type) {
- // Remove the type arguments from generic type references, because
- // they'll be specified as type arguments to `inject()`.
- if (ts.isTypeReferenceNode(type) && type.typeArguments && type.typeArguments.length > 0) {
- injectedType = type.typeName.getText();
- }
- else if (ts.isUnionTypeNode(type)) {
- injectedType = (type.types.find((t) => !ts.isLiteralTypeNode(t)) || type.types[0]).getText();
- }
- else {
- injectedType = type.getText();
- }
- }
- for (const decorator of decorators) {
- if (decorator.moduleName !== moduleName) {
- continue;
- }
- const firstArg = decorator.node.expression.arguments[0];
- switch (decorator.name) {
- case 'Inject':
- if (firstArg) {
- const injectResult = migrateInjectDecorator(firstArg, type, localTypeChecker);
- injectedType = injectResult.injectedType;
- if (injectResult.typeArguments) {
- typeArguments = injectResult.typeArguments;
- }
- }
- break;
- case 'Attribute':
- if (firstArg) {
- const constructorRef = tracker.addImport(sourceFile, 'HostAttributeToken', moduleName);
- const expression = ts.factory.createNewExpression(constructorRef, undefined, [firstArg]);
- injectedType = printer.printNode(ts.EmitHint.Unspecified, expression, sourceFile);
- typeArguments = undefined;
- }
- break;
- case 'Optional':
- hasOptionalDecorator = true;
- literalProps.push(ts.factory.createPropertyAssignment('optional', ts.factory.createTrue()));
- break;
- case 'SkipSelf':
- literalProps.push(ts.factory.createPropertyAssignment('skipSelf', ts.factory.createTrue()));
- break;
- case 'Self':
- literalProps.push(ts.factory.createPropertyAssignment('self', ts.factory.createTrue()));
- break;
- case 'Host':
- literalProps.push(ts.factory.createPropertyAssignment('host', ts.factory.createTrue()));
- break;
- }
- }
- // The injected type might be a `TypeNode` which we can't easily convert into an `Expression`.
- // Since the value gets passed through directly anyway, we generate the call using a placeholder
- // which we then replace with the raw text of the `TypeNode`.
- const injectRef = tracker.addImport(param.getSourceFile(), 'inject', moduleName);
- const args = [ts.factory.createIdentifier(PLACEHOLDER)];
- if (literalProps.length > 0) {
- args.push(ts.factory.createObjectLiteralExpression(literalProps));
- }
- let expression = ts.factory.createCallExpression(injectRef, typeArguments, args);
- if (hasOptionalDecorator && options.nonNullableOptional) {
- const hasNullableType = param.questionToken != null || (param.type != null && isNullableType(param.type));
- // Only wrap the expression if the type wasn't already nullable.
- // If it was, the app was likely accounting for it already.
- if (!hasNullableType) {
- expression = ts.factory.createNonNullExpression(expression);
- }
- }
- // If the parameter is initialized, add the initializer as a fallback.
- if (param.initializer) {
- expression = ts.factory.createBinaryExpression(expression, ts.SyntaxKind.QuestionQuestionToken, param.initializer);
- }
- return replaceNodePlaceholder(param.getSourceFile(), expression, injectedType, printer);
- }
- /**
- * Migrates a parameter based on its `@Inject()` decorator.
- * @param firstArg First argument to `@Inject()`.
- * @param type Type of the parameter.
- * @param localTypeChecker Type checker set up for the specific file.
- */
- function migrateInjectDecorator(firstArg, type, localTypeChecker) {
- let injectedType = firstArg.getText();
- let typeArguments = null;
- // `inject` no longer officially supports string injection so we need
- // to cast to any. We maintain the type by passing it as a generic.
- if (ts.isStringLiteralLike(firstArg)) {
- typeArguments = [type || ts.factory.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)];
- injectedType += ' as any';
- }
- else if (ts.isCallExpression(firstArg) &&
- ts.isIdentifier(firstArg.expression) &&
- firstArg.arguments.length === 1) {
- const callImport = imports.getImportOfIdentifier(localTypeChecker, firstArg.expression);
- const arrowFn = firstArg.arguments[0];
- // If the first parameter is a `forwardRef`, unwrap it for a more
- // accurate type and because it's no longer necessary.
- if (callImport !== null &&
- callImport.name === 'forwardRef' &&
- callImport.importModule === '@angular/core' &&
- ts.isArrowFunction(arrowFn)) {
- if (ts.isBlock(arrowFn.body)) {
- const returnStatement = arrowFn.body.statements.find((stmt) => ts.isReturnStatement(stmt));
- if (returnStatement && returnStatement.expression) {
- injectedType = returnStatement.expression.getText();
- }
- }
- else {
- injectedType = arrowFn.body.getText();
- }
- }
- }
- else if (type &&
- (ts.isTypeReferenceNode(type) ||
- ts.isTypeLiteralNode(type) ||
- ts.isTupleTypeNode(type) ||
- (ts.isUnionTypeNode(type) && type.types.some(ts.isTypeReferenceNode)))) {
- typeArguments = [type];
- }
- return { injectedType, typeArguments };
- }
- /**
- * Removes the parameters from a constructor. This is a bit more complex than just replacing an AST
- * node, because `NodeArray.pos` includes any leading whitespace, but `NodeArray.end` does **not**
- * include trailing whitespace. Since we want to produce somewhat formatted code, we need to find
- * the end of the arguments ourselves. We do it by finding the next parenthesis after the last
- * parameter.
- * @param node Constructor from which to remove the parameters.
- * @param tracker Object keeping track of the changes made to the file.
- */
- function stripConstructorParameters(node, tracker) {
- if (node.parameters.length === 0) {
- return;
- }
- const constructorText = node.getText();
- const lastParamText = node.parameters[node.parameters.length - 1].getText();
- const lastParamStart = constructorText.indexOf(lastParamText);
- // This shouldn't happen, but bail out just in case so we don't mangle the code.
- if (lastParamStart === -1) {
- return;
- }
- for (let i = lastParamStart + lastParamText.length; i < constructorText.length; i++) {
- const char = constructorText[i];
- if (char === ')') {
- tracker.replaceText(node.getSourceFile(), node.parameters.pos, node.getStart() + i - node.parameters.pos, '');
- break;
- }
- }
- }
- /**
- * Creates a type checker scoped to a specific file.
- * @param sourceFile File for which to create the type checker.
- */
- function getLocalTypeChecker(sourceFile) {
- const options = { noEmit: true, skipLibCheck: true };
- const host = ts.createCompilerHost(options);
- host.getSourceFile = (fileName) => (fileName === sourceFile.fileName ? sourceFile : undefined);
- const program = ts.createProgram({
- rootNames: [sourceFile.fileName],
- options,
- host,
- });
- return program.getTypeChecker();
- }
- /**
- * Prints out an AST node and replaces the placeholder inside of it.
- * @param sourceFile File in which the node will be inserted.
- * @param node Node to be printed out.
- * @param replacement Replacement for the placeholder.
- * @param printer Printer used to output AST nodes as strings.
- */
- function replaceNodePlaceholder(sourceFile, node, replacement, printer) {
- const result = printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);
- return result.replace(PLACEHOLDER, replacement);
- }
- /**
- * Clones an optional array of modifiers. Can be useful to
- * strip the comments from a node with modifiers.
- */
- function cloneModifiers(modifiers) {
- return modifiers?.map((modifier) => {
- return ts.isDecorator(modifier)
- ? ts.factory.createDecorator(modifier.expression)
- : ts.factory.createModifier(modifier.kind);
- });
- }
- /**
- * Clones the name of a property. Can be useful to strip away
- * the comments of a property without modifiers.
- */
- function cloneName(node) {
- switch (node.kind) {
- case ts.SyntaxKind.Identifier:
- return ts.factory.createIdentifier(node.text);
- case ts.SyntaxKind.StringLiteral:
- return ts.factory.createStringLiteral(node.text, node.getText()[0] === `'`);
- case ts.SyntaxKind.NoSubstitutionTemplateLiteral:
- return ts.factory.createNoSubstitutionTemplateLiteral(node.text, node.rawText);
- case ts.SyntaxKind.NumericLiteral:
- return ts.factory.createNumericLiteral(node.text);
- case ts.SyntaxKind.ComputedPropertyName:
- return ts.factory.createComputedPropertyName(node.expression);
- case ts.SyntaxKind.PrivateIdentifier:
- return ts.factory.createPrivateIdentifier(node.text);
- default:
- return node;
- }
- }
- /**
- * Determines whether it's safe to delete a class constructor.
- * @param options Options used to configure the migration.
- * @param constructor Node representing the constructor.
- * @param removedStatementCount Number of statements that were removed by the migration.
- * @param prependToConstructor Statements that should be prepended to the constructor.
- * @param superCall Node representing the `super()` call within the constructor.
- */
- function canRemoveConstructor(options, constructor, removedStatementCount, prependToConstructor, superCall) {
- if (options.backwardsCompatibleConstructors || prependToConstructor.length > 0) {
- return false;
- }
- const statementCount = constructor.body
- ? constructor.body.statements.length - removedStatementCount
- : 0;
- return (statementCount === 0 ||
- (statementCount === 1 && superCall !== null && superCall.arguments.length === 0));
- }
- /**
- * Gets the next statement after a node that *won't* be deleted by the migration.
- * @param startNode Node from which to start the search.
- * @param removedStatements Statements that have been removed by the migration.
- * @returns
- */
- function getNextPreservedStatement(startNode, removedStatements) {
- const body = nodes.closestNode(startNode, ts.isBlock);
- const closestStatement = nodes.closestNode(startNode, ts.isStatement);
- if (body === null || closestStatement === null) {
- return null;
- }
- const index = body.statements.indexOf(closestStatement);
- if (index === -1) {
- return null;
- }
- for (let i = index + 1; i < body.statements.length; i++) {
- if (!removedStatements.has(body.statements[i])) {
- return body.statements[i];
- }
- }
- return null;
- }
- /**
- * Applies the internal-specific migrations to a class.
- * @param node Class being migrated.
- * @param constructor The migrated class' constructor.
- * @param localTypeChecker File-specific type checker.
- * @param tracker Object keeping track of the changes.
- * @param printer Printer used to output AST nodes as text.
- * @param removedStatements Statements that have been removed by the migration.
- * @param removedMembers Class members that have been removed by the migration.
- * @param prependToClass Text that will be prepended to a class.
- * @param afterInjectCalls Text that will be inserted after the newly-added `inject` calls.
- * @param memberIndentation Indentation string of the class' members.
- */
- function applyInternalOnlyChanges(node, constructor, localTypeChecker, tracker, printer, removedStatements, removedMembers, prependToClass, afterInjectCalls, memberIndentation) {
- const result = findUninitializedPropertiesToCombine(node, constructor, localTypeChecker);
- if (result === null) {
- return;
- }
- const preserveInitOrder = shouldCombineInInitializationOrder(result.toCombine, constructor);
- // Sort the combined members based on the declaration order of their initializers, only if
- // we've determined that would be safe. Note that `Array.prototype.sort` is in-place so we
- // can just call it conditionally here.
- if (preserveInitOrder) {
- result.toCombine.sort((a, b) => a.initializer.getStart() - b.initializer.getStart());
- }
- result.toCombine.forEach(({ declaration, initializer }) => {
- const initializerStatement = nodes.closestNode(initializer, ts.isStatement);
- const newProperty = ts.factory.createPropertyDeclaration(cloneModifiers(declaration.modifiers), cloneName(declaration.name), declaration.questionToken, declaration.type, initializer);
- // If the initialization order is being preserved, we have to remove the original
- // declaration and re-declare it. Otherwise we can do the replacement in-place.
- if (preserveInitOrder) {
- tracker.removeNode(declaration, true);
- removedMembers.add(declaration);
- afterInjectCalls.push(memberIndentation +
- printer.printNode(ts.EmitHint.Unspecified, newProperty, declaration.getSourceFile()));
- }
- else {
- tracker.replaceNode(declaration, newProperty);
- }
- // This should always be defined, but null check it just in case.
- if (initializerStatement) {
- tracker.removeNode(initializerStatement, true);
- removedStatements.add(initializerStatement);
- }
- });
- result.toHoist.forEach((decl) => {
- prependToClass.push(memberIndentation + printer.printNode(ts.EmitHint.Unspecified, decl, decl.getSourceFile()));
- tracker.removeNode(decl, true);
- removedMembers.add(decl);
- });
- // If we added any hoisted properties, separate them visually with a new line.
- if (prependToClass.length > 0) {
- prependToClass.push('');
- }
- }
- function migrate(options) {
- return async (tree) => {
- const basePath = process.cwd();
- const pathToMigrate = compiler_host.normalizePath(p.join(basePath, options.path));
- let allPaths = [];
- if (pathToMigrate.trim() !== '') {
- allPaths.push(pathToMigrate);
- }
- if (!allPaths.length) {
- throw new schematics.SchematicsException('Could not find any tsconfig file. Cannot run the inject migration.');
- }
- for (const tsconfigPath of allPaths) {
- runInjectMigration(tree, tsconfigPath, basePath, pathToMigrate, options);
- }
- };
- }
- function runInjectMigration(tree, tsconfigPath, basePath, pathToMigrate, schematicOptions) {
- if (schematicOptions.path.startsWith('..')) {
- throw new schematics.SchematicsException('Cannot run inject migration outside of the current project.');
- }
- const program = compiler_host.createMigrationProgram(tree, tsconfigPath, basePath);
- const sourceFiles = program
- .getSourceFiles()
- .filter((sourceFile) => sourceFile.fileName.startsWith(pathToMigrate) &&
- compiler_host.canMigrateFile(basePath, sourceFile, program));
- if (sourceFiles.length === 0) {
- throw new schematics.SchematicsException(`Could not find any files to migrate under the path ${pathToMigrate}. Cannot run the inject migration.`);
- }
- for (const sourceFile of sourceFiles) {
- const changes = migrateFile(sourceFile, schematicOptions);
- if (changes.length > 0) {
- const update = tree.beginUpdate(p.relative(basePath, sourceFile.fileName));
- for (const change of changes) {
- if (change.removeLength != null) {
- update.remove(change.start, change.removeLength);
- }
- update.insertRight(change.start, change.text);
- }
- tree.commitUpdate(update);
- }
- }
- }
- exports.migrate = migrate;
|