Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ ts_library(
"//packages/compiler-cli",
"//packages/compiler-cli/private",
"//packages/compiler-cli/src/ngtsc/core:api",
"//packages/compiler-cli/src/ngtsc/file_system",
"//packages/core/schematics/utils",
"//packages/core/schematics/utils/tsurge",
"//packages/core/schematics/utils/tsurge/helpers/angular_devkit",
Expand Down
111 changes: 34 additions & 77 deletions packages/core/schematics/ng-generate/cleanup-unused-imports/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,86 +6,43 @@
* found in the LICENSE file at https://angular.dev/license
*/

import {Rule, SchematicsException} from '@angular-devkit/schematics';

import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
import {DevkitMigrationFilesystem} from '../../utils/tsurge/helpers/angular_devkit/devkit_filesystem';
import {groupReplacementsByFile} from '../../utils/tsurge/helpers/group_replacements';
import {setFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system';
import {ProjectRootRelativePath, TextUpdate} from '../../utils/tsurge';
import {synchronouslyCombineUnitData} from '../../utils/tsurge/helpers/combine_units';
import {CompilationUnitData, UnusedImportsMigration} from './unused_imports_migration';
import {Rule} from '@angular-devkit/schematics';
import {MigrationStage, runMigrationInDevkit} from '../../utils/tsurge/helpers/angular_devkit';
import {UnusedImportsMigration} from './unused_imports_migration';

export function migrate(): Rule {
return async (tree, context) => {
const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree);

if (!buildPaths.length && !testPaths.length) {
throw new SchematicsException(
'Could not find any tsconfig file. Cannot clean up unused imports.',
);
}

const fs = new DevkitMigrationFilesystem(tree);
setFileSystem(fs);

const migration = new UnusedImportsMigration();
const unitResults: CompilationUnitData[] = [];
const programInfos = [...buildPaths, ...testPaths].map((tsconfigPath) => {
context.logger.info(`Preparing analysis for ${tsconfigPath}`);

const baseInfo = migration.createProgram(tsconfigPath, fs);
const info = migration.prepareProgram(baseInfo);

return {info, tsconfigPath};
await runMigrationInDevkit({
getMigration: () => new UnusedImportsMigration(),
tree,
beforeProgramCreation: (tsconfigPath, stage) => {
if (stage === MigrationStage.Analysis) {
context.logger.info(`Preparing analysis for: ${tsconfigPath}...`);
} else {
context.logger.info(`Running migration for: ${tsconfigPath}...`);
}
},
beforeUnitAnalysis: (tsconfigPath) => {
context.logger.info(`Scanning for unused imports using ${tsconfigPath}`);
},
afterAnalysisFailure: () => {
context.logger.error('Schematic failed unexpectedly with no analysis data');
},
whenDone: (stats) => {
const {removedImports, changedFiles} = stats.counters;
let statsMessage: string;

if (removedImports === 0) {
statsMessage = 'Schematic could not find unused imports in the project';
} else {
statsMessage =
`Removed ${removedImports} import${removedImports !== 1 ? 's' : ''} ` +
`in ${changedFiles} file${changedFiles !== 1 ? 's' : ''}`;
}

context.logger.info('');
context.logger.info(statsMessage);
},
});

for (const {info, tsconfigPath} of programInfos) {
context.logger.info(`Scanning for unused imports using ${tsconfigPath}`);
unitResults.push(await migration.analyze(info));
}

const combined = await synchronouslyCombineUnitData(migration, unitResults);
if (combined === null) {
context.logger.error('Schematic failed unexpectedly with no analysis data');
return;
}

const globalMeta = await migration.globalMeta(combined);
const replacementsPerFile: Map<ProjectRootRelativePath, TextUpdate[]> = new Map();
const {replacements} = await migration.migrate(globalMeta);
const changesPerFile = groupReplacementsByFile(replacements);

for (const [file, changes] of changesPerFile) {
if (!replacementsPerFile.has(file)) {
replacementsPerFile.set(file, changes);
}
}

for (const [file, changes] of replacementsPerFile) {
const recorder = tree.beginUpdate(file);
for (const c of changes) {
recorder
.remove(c.data.position, c.data.end - c.data.position)
.insertRight(c.data.position, c.data.toInsert);
}
tree.commitUpdate(recorder);
}

const {
counters: {removedImports, changedFiles},
} = await migration.stats(globalMeta);
let statsMessage: string;

if (removedImports === 0) {
statsMessage = 'Schematic could not find unused imports in the project';
} else {
statsMessage =
`Removed ${removedImports} import${removedImports !== 1 ? 's' : ''} ` +
`in ${changedFiles} file${changedFiles !== 1 ? 's' : ''}`;
}

context.logger.info('');
context.logger.info(statsMessage);
};
}
155 changes: 52 additions & 103 deletions packages/core/schematics/ng-generate/output-migration/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,9 @@
* found in the LICENSE file at https://angular.dev/license
*/

import {Rule, SchematicsException} from '@angular-devkit/schematics';

import {getProjectTsConfigPaths} from '../../utils/project_tsconfig_paths';
import {DevkitMigrationFilesystem} from '../../utils/tsurge/helpers/angular_devkit/devkit_filesystem';
import {groupReplacementsByFile} from '../../utils/tsurge/helpers/group_replacements';
import {setFileSystem} from '@angular/compiler-cli/src/ngtsc/file_system';
import {
CompilationUnitData,
OutputMigration,
} from '../../migrations/output-migration/output-migration';
import {ProjectRootRelativePath, TextUpdate} from '../../utils/tsurge';
import {synchronouslyCombineUnitData} from '../../utils/tsurge/helpers/combine_units';
import {Rule} from '@angular-devkit/schematics';
import {OutputMigration} from '../../migrations/output-migration/output-migration';
import {MigrationStage, runMigrationInDevkit} from '../../utils/tsurge/helpers/angular_devkit';

interface Options {
path: string;
Expand All @@ -26,99 +17,57 @@ interface Options {

export function migrate(options: Options): Rule {
return async (tree, context) => {
const {buildPaths, testPaths} = await getProjectTsConfigPaths(tree);

if (!buildPaths.length && !testPaths.length) {
throw new SchematicsException(
'Could not find any tsconfig file. Cannot run output migration.',
);
}

const fs = new DevkitMigrationFilesystem(tree);
setFileSystem(fs);

const migration = new OutputMigration({
shouldMigrate: (_, file) => {
return (
file.rootRelativePath.startsWith(fs.normalize(options.path)) &&
!/(^|\/)node_modules\//.test(file.rootRelativePath)
);
await runMigrationInDevkit({
tree,
getMigration: (fs) =>
new OutputMigration({
shouldMigrate: (_, file) => {
return (
file.rootRelativePath.startsWith(fs.normalize(options.path)) &&
!/(^|\/)node_modules\//.test(file.rootRelativePath)
);
},
}),
beforeProgramCreation: (tsconfigPath, stage) => {
if (stage === MigrationStage.Analysis) {
context.logger.info(`Preparing analysis for: ${tsconfigPath}...`);
} else {
context.logger.info(`Running migration for: ${tsconfigPath}...`);
}
},
});

const analysisPath = fs.resolve(options.analysisDir);
const unitResults: CompilationUnitData[] = [];
const programInfos = [...buildPaths, ...testPaths].map((tsconfigPath) => {
context.logger.info(`Preparing analysis for: ${tsconfigPath}..`);

const baseInfo = migration.createProgram(tsconfigPath, fs);
const info = migration.prepareProgram(baseInfo);

// Support restricting the analysis to subfolders for larger projects.
if (analysisPath !== '/') {
info.sourceFiles = info.sourceFiles.filter((sf) => sf.fileName.startsWith(analysisPath));
info.fullProgramSourceFiles = info.fullProgramSourceFiles.filter((sf) =>
sf.fileName.startsWith(analysisPath),
afterProgramCreation: (info, fs) => {
const analysisPath = fs.resolve(options.analysisDir);

// Support restricting the analysis to subfolders for larger projects.
if (analysisPath !== '/') {
info.sourceFiles = info.sourceFiles.filter((sf) => sf.fileName.startsWith(analysisPath));
info.fullProgramSourceFiles = info.fullProgramSourceFiles.filter((sf) =>
sf.fileName.startsWith(analysisPath),
);
}
},
beforeUnitAnalysis: (tsconfigPath) => {
context.logger.info(`Scanning for outputs: ${tsconfigPath}...`);
},
afterAllAnalyzed: () => {
context.logger.info(``);
context.logger.info(`Processing analysis data between targets...`);
context.logger.info(``);
},
afterAnalysisFailure: () => {
context.logger.error('Migration failed unexpectedly with no analysis data');
},
whenDone: ({counters}) => {
const {detectedOutputs, problematicOutputs, successRate} = counters;
const migratedOutputs = detectedOutputs - problematicOutputs;
const successRatePercent = (successRate * 100).toFixed(2);

context.logger.info('');
context.logger.info(`Successfully migrated to outputs as functions 🎉`);
context.logger.info(
` -> Migrated ${migratedOutputs} out of ${detectedOutputs} detected outputs (${successRatePercent} %).`,
);
}

return {info, tsconfigPath};
},
});

// Analyze phase. Treat all projects as compilation units as
// this allows us to support references between those.
for (const {info, tsconfigPath} of programInfos) {
context.logger.info(`Scanning for outputs: ${tsconfigPath}..`);
unitResults.push(await migration.analyze(info));
}

context.logger.info(``);
context.logger.info(`Processing analysis data between targets..`);
context.logger.info(``);

const combined = await synchronouslyCombineUnitData(migration, unitResults);
if (combined === null) {
context.logger.error('Migration failed unexpectedly with no analysis data');
return;
}

const globalMeta = await migration.globalMeta(combined);
const replacementsPerFile: Map<ProjectRootRelativePath, TextUpdate[]> = new Map();

for (const {info, tsconfigPath} of programInfos) {
context.logger.info(`Migrating: ${tsconfigPath}..`);

const {replacements} = await migration.migrate(globalMeta);
const changesPerFile = groupReplacementsByFile(replacements);

for (const [file, changes] of changesPerFile) {
if (!replacementsPerFile.has(file)) {
replacementsPerFile.set(file, changes);
}
}
}

context.logger.info(`Applying changes..`);
for (const [file, changes] of replacementsPerFile) {
const recorder = tree.beginUpdate(file);
for (const c of changes) {
recorder
.remove(c.data.position, c.data.end - c.data.position)
.insertLeft(c.data.position, c.data.toInsert);
}
tree.commitUpdate(recorder);
}

const {
counters: {detectedOutputs, problematicOutputs, successRate},
} = await migration.stats(globalMeta);
const migratedOutputs = detectedOutputs - problematicOutputs;
const successRatePercent = (successRate * 100).toFixed(2);

context.logger.info('');
context.logger.info(`Successfully migrated to outputs as functions 🎉`);
context.logger.info(
` -> Migrated ${migratedOutputs} out of ${detectedOutputs} detected outputs (${successRatePercent} %).`,
);
};
}
Loading