-
Notifications
You must be signed in to change notification settings - Fork 34
fix: adjust typescript imports to work on case sensitive filesystem #804
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 2 commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
12c0b6b
fix: adjust typescript imports to work on case sensitive filesystem
johnhwhite 1af146b
Fix prettier
johnhwhite 08303d1
switch from ts printer to find/replace to prevent formatting changes
johnhwhite 6af6826
switch to using test scaffold
johnhwhite File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
11 changes: 11 additions & 0 deletions
11
...ages/src/schematics/migrations/update-7/__snapshots__/fix-mixed-case-imports.spec.ts.snap
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,11 @@ | ||
| // Jest Snapshot v1, https://goo.gl/fbAQLP | ||
|
|
||
| exports[`fix mixed case imports should change import paths that are incorrect 1`] = ` | ||
| "import { A } from './a'; | ||
| import { C } from './Path/C'; | ||
| import { D } from './Path/d'; | ||
| import { NonExistent } from './non'; | ||
| export class B extends A { | ||
| } | ||
| " | ||
| `; |
69 changes: 69 additions & 0 deletions
69
libs/components/packages/src/schematics/migrations/update-7/fix-mixed-case-imports.spec.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| import { Tree } from '@angular-devkit/schematics'; | ||
| import { SchematicTestRunner } from '@angular-devkit/schematics/testing'; | ||
|
|
||
| describe('fix mixed case imports', () => { | ||
| let tree: Tree; | ||
| const runner = new SchematicTestRunner( | ||
| 'schematics', | ||
| require.resolve('../migration-collection.json') | ||
| ); | ||
| const angularJson = { | ||
| version: 1, | ||
| projects: { | ||
| test: { | ||
| projectType: 'application', | ||
| root: '', | ||
| architect: {}, | ||
| }, | ||
| }, | ||
| }; | ||
|
|
||
| function setupTest() { | ||
| tree = Tree.empty(); | ||
| tree.create('/angular.json', JSON.stringify(angularJson)); | ||
| } | ||
|
|
||
| it('should not change import paths that are correct', async () => { | ||
| setupTest(); | ||
| tree.create('/index.ts', `import { A } from './a';`); | ||
| tree.create('/a.ts', `export class A {}`); | ||
| tree.create('/empty.ts', ``); | ||
| await runner | ||
| .runSchematicAsync('fix-mixed-case-imports', {}, tree) | ||
| .toPromise(); | ||
| expect(tree.readText('/index.ts')).toBe(`import { A } from './a';`); | ||
| }); | ||
|
|
||
| it('should update import paths with backslashes', async () => { | ||
| setupTest(); | ||
| tree.create('/windows.ts', "import { A } from '.\\a';"); | ||
| tree.create('/a.ts', `export class A {}`); | ||
| await runner | ||
| .runSchematicAsync('fix-mixed-case-imports', {}, tree) | ||
| .toPromise(); | ||
| expect(tree.readText('/windows.ts')).toBe(`import { A } from './a';\n`); | ||
| }); | ||
|
|
||
| it('should change import paths that are incorrect', async () => { | ||
| setupTest(); | ||
| tree.create( | ||
| '/index.ts', | ||
| [ | ||
| `import { A } from './A';`, | ||
| `import { C } from './path/c';`, | ||
| `import { D } from './Path/D';`, | ||
| `import { NonExistent } from './non';`, | ||
| `export class B extends A {`, | ||
| `}`, | ||
| '', | ||
| ].join(`\n`) | ||
| ); | ||
| tree.create('/a.ts', `export class A {}`); | ||
| tree.create('/Path/C.ts', `export class C {}`); | ||
| tree.create('/Path/d.ts', `export class D {}`); | ||
| await runner | ||
| .runSchematicAsync('fix-mixed-case-imports', {}, tree) | ||
| .toPromise(); | ||
| expect(tree.readText('/index.ts')).toMatchSnapshot(); | ||
| }); | ||
| }); | ||
152 changes: 152 additions & 0 deletions
152
libs/components/packages/src/schematics/migrations/update-7/fix-mixed-case-imports.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,152 @@ | ||
| import { Path, dirname, join, split } from '@angular-devkit/core'; | ||
| import { Rule, Tree } from '@angular-devkit/schematics'; | ||
| import * as ts from '@schematics/angular/third_party/github.com/Microsoft/TypeScript/lib/typescript'; | ||
|
|
||
| import * as path from 'path'; | ||
|
|
||
| import { getWorkspace } from '../../utility/workspace'; | ||
|
|
||
| function updateTypescriptImports(filePath: string, tree: Tree): void { | ||
| const fileContent = tree.read(filePath)?.toString(); | ||
| if (!fileContent) { | ||
| return; | ||
| } | ||
| const source = ts.createSourceFile( | ||
| filePath, | ||
| fileContent, | ||
| ts.ScriptTarget.Latest, | ||
| true | ||
| ); | ||
| // Get all import declarations | ||
| const importDeclarations = source.statements.filter( | ||
| (statement) => statement.kind === ts.SyntaxKind.ImportDeclaration | ||
| ) as ts.ImportDeclaration[]; | ||
| // Just local import targets | ||
| const localImportDeclarations = importDeclarations.filter( | ||
| (importDeclaration) => { | ||
| const importPath = importDeclaration.moduleSpecifier.getText(source); | ||
| const importPathWithoutQuotes = importPath.substring( | ||
| 1, | ||
| importPath.length - 1 | ||
| ); | ||
| return importPathWithoutQuotes.startsWith('.'); | ||
| } | ||
| ); | ||
| const transformers: ts.TransformerFactory<ts.SourceFile>[] = []; | ||
| localImportDeclarations.forEach((importDeclaration) => { | ||
| const importPath = importDeclaration.moduleSpecifier.getText(source); | ||
| const importPathWithoutQuotes = importPath.substring( | ||
| 1, | ||
| importPath.length - 1 | ||
| ); | ||
| const absoluteImportPath = path.join( | ||
| path.dirname(filePath), | ||
| `${importPathWithoutQuotes}.ts`.replace(/\\/g, '/') | ||
| ); | ||
| const fileEntry = tree.get(absoluteImportPath); | ||
| if ( | ||
| fileEntry && | ||
| fileEntry.path === absoluteImportPath && | ||
| !importPathWithoutQuotes.includes('\\') | ||
| ) { | ||
| // File exists. Do nothing. | ||
| return; | ||
| } | ||
| const pathFragments = split(absoluteImportPath as Path).slice(1); | ||
| const newPath: string[] = []; | ||
| let dir = tree.root; | ||
| for (const pathFragment of pathFragments) { | ||
| if (pathFragment.endsWith('.ts')) { | ||
| if (dir.subfiles.includes(pathFragment)) { | ||
| // File exists. We're done. | ||
| newPath.push(pathFragment.replace(/\.ts$/, '')); | ||
| break; | ||
| } | ||
| // File does not exist. Try to find a file with the same name but different casing. | ||
| const matchingFile = dir.subfiles.find((subFile) => | ||
| subFile.toLowerCase().includes(pathFragment.toLowerCase()) | ||
| ); | ||
| if (matchingFile) { | ||
| // Found a matching file. Use it. | ||
| newPath.push(matchingFile.replace(/\.ts$/, '')); | ||
| break; | ||
| } | ||
| } else { | ||
| if (dir.subdirs.includes(pathFragment)) { | ||
| // Sub dir exists. Continue. | ||
| dir = dir.dir(pathFragment); | ||
| newPath.push(pathFragment); | ||
| continue; | ||
| } | ||
| // Dir does not exist. Try to find a dir with the same name but different casing. | ||
| const matchingDir = dir.subdirs.find((subdir) => | ||
| subdir.toLowerCase().includes(pathFragment.toLowerCase()) | ||
| ); | ||
| if (matchingDir) { | ||
| // Found a matching dir. Use it. | ||
| dir = dir.dir(matchingDir); | ||
| newPath.push(matchingDir); | ||
| continue; | ||
| } | ||
| } | ||
| // Nothing found. Can't fix this. | ||
| return; | ||
| } | ||
| const newImportPathAbsolute = join(tree.root.path, ...newPath); | ||
| let newImportPathRelative = path.relative( | ||
| dirname(filePath as Path), | ||
| newImportPathAbsolute | ||
| ); | ||
| if (!newImportPathRelative.startsWith('.')) { | ||
| newImportPathRelative = `./${newImportPathRelative}`; | ||
| } | ||
| if (newImportPathRelative !== importPathWithoutQuotes) { | ||
| transformers.push((transformationContext) => (file) => { | ||
| const visitor = (node: ts.Node): ts.Node => { | ||
| if (ts.isImportDeclaration(node) && node === importDeclaration) { | ||
| return transformationContext.factory.updateImportDeclaration( | ||
| node as ts.ImportDeclaration, | ||
| node.decorators, | ||
| node.modifiers, | ||
| node.importClause, | ||
| transformationContext.factory.createStringLiteral( | ||
| newImportPathRelative, | ||
| true | ||
| ), | ||
| node.assertClause | ||
| ); | ||
| } | ||
| return ts.visitEachChild(node, visitor, transformationContext); | ||
| }; | ||
| return ts.visitNode(file, visitor); | ||
| }); | ||
| } | ||
| }); | ||
| if (transformers.length > 0) { | ||
| const printer = ts.createPrinter({ newLine: ts.NewLineKind.LineFeed }); | ||
| const result = ts.transform([source], transformers); | ||
| const newContent = printer.printNode( | ||
| ts.EmitHint.Unspecified, | ||
| result.transformed[0], | ||
| undefined as any | ||
| ); | ||
| tree.overwrite(filePath, newContent); | ||
| } | ||
| } | ||
|
|
||
| async function visitTypescriptFiles(tree: Tree): Promise<void> { | ||
| const { workspace } = await getWorkspace(tree); | ||
| workspace.projects.forEach((project) => { | ||
| tree.getDir(project.root).visit((filePath) => { | ||
| if (filePath.endsWith('.ts')) { | ||
| updateTypescriptImports(filePath, tree); | ||
| } | ||
| }); | ||
| }); | ||
| } | ||
|
|
||
| export default function (): Rule { | ||
| return async (tree: Tree) => { | ||
| await visitTypescriptFiles(tree); | ||
| }; | ||
| } |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.