-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(angular): add backwards support to component (#13583)
- Loading branch information
Showing
9 changed files
with
1,262 additions
and
0 deletions.
There are no files selected for viewing
157 changes: 157 additions & 0 deletions
157
packages/angular/src/generators/component/__snapshots__/component.v14.spec.ts.snap
This file contains 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,157 @@ | ||
// Jest Snapshot v1, https://goo.gl/fbAQLP | ||
|
||
exports[`component Generator support angular 14 --flat should create the component correctly and export it in the entry point 1`] = ` | ||
"import { Component } from '@angular/core'; | ||
@Component({ | ||
selector: 'example', | ||
templateUrl: './example.component.html', | ||
styleUrls: ['./example.component.css'] | ||
}) | ||
export class ExampleComponent { | ||
} | ||
" | ||
`; | ||
|
||
exports[`component Generator support angular 14 --flat should create the component correctly and not export it when "export=false" 1`] = ` | ||
"import { Component } from '@angular/core'; | ||
@Component({ | ||
selector: 'example', | ||
templateUrl: './example.component.html', | ||
styleUrls: ['./example.component.css'] | ||
}) | ||
export class ExampleComponent { | ||
} | ||
" | ||
`; | ||
|
||
exports[`component Generator support angular 14 --path should create the component correctly and export it in the entry point 1`] = ` | ||
"import { Component } from '@angular/core'; | ||
@Component({ | ||
selector: 'example', | ||
templateUrl: './example.component.html', | ||
styleUrls: ['./example.component.css'] | ||
}) | ||
export class ExampleComponent { | ||
} | ||
" | ||
`; | ||
|
||
exports[`component Generator support angular 14 secondary entry points should create the component correctly and export it in the entry point 1`] = ` | ||
"import { Component } from '@angular/core'; | ||
@Component({ | ||
selector: 'example', | ||
templateUrl: './example.component.html', | ||
styleUrls: ['./example.component.css'] | ||
}) | ||
export class ExampleComponent { | ||
} | ||
" | ||
`; | ||
|
||
exports[`component Generator support angular 14 secondary entry points should create the component correctly and export it in the entry point 2`] = ` | ||
"export * from \\"./lib/secondary.module\\"; | ||
export * from \\"./lib/example/example.component\\";" | ||
`; | ||
|
||
exports[`component Generator support angular 14 should create the component correctly and export it in the entry point when "export=true" 1`] = ` | ||
"import { Component } from '@angular/core'; | ||
@Component({ | ||
selector: 'example', | ||
templateUrl: './example.component.html', | ||
styleUrls: ['./example.component.css'] | ||
}) | ||
export class ExampleComponent { | ||
} | ||
" | ||
`; | ||
|
||
exports[`component Generator support angular 14 should create the component correctly and export it in the entry point when "export=true" 2`] = ` | ||
"export * from \\"./lib/lib.module\\"; | ||
export * from \\"./lib/example/example.component\\";" | ||
`; | ||
|
||
exports[`component Generator support angular 14 should create the component correctly and export it in the entry point when is standalone and "export=true" 1`] = ` | ||
"import { Component } from '@angular/core'; | ||
import { CommonModule } from '@angular/common'; | ||
@Component({ | ||
selector: 'example', | ||
standalone: true, | ||
imports: [CommonModule], | ||
templateUrl: './example.component.html', | ||
styleUrls: ['./example.component.css'] | ||
}) | ||
export class ExampleComponent { | ||
} | ||
" | ||
`; | ||
|
||
exports[`component Generator support angular 14 should create the component correctly and not export it in the entry point when "export=false" 1`] = ` | ||
"import { Component } from '@angular/core'; | ||
@Component({ | ||
selector: 'example', | ||
templateUrl: './example.component.html', | ||
styleUrls: ['./example.component.css'] | ||
}) | ||
export class ExampleComponent { | ||
} | ||
" | ||
`; | ||
|
||
exports[`component Generator support angular 14 should create the component correctly and not export it in the entry point when is standalone and "export=false" 1`] = ` | ||
"import { Component } from '@angular/core'; | ||
import { CommonModule } from '@angular/common'; | ||
@Component({ | ||
selector: 'example', | ||
standalone: true, | ||
imports: [CommonModule], | ||
templateUrl: './example.component.html', | ||
styleUrls: ['./example.component.css'] | ||
}) | ||
export class ExampleComponent { | ||
} | ||
" | ||
`; | ||
|
||
exports[`component Generator support angular 14 should create the component correctly and not export it when "--skip-import=true" 1`] = ` | ||
"import { Component } from '@angular/core'; | ||
@Component({ | ||
selector: 'example', | ||
templateUrl: './example.component.html', | ||
styleUrls: ['./example.component.css'] | ||
}) | ||
export class ExampleComponent { | ||
} | ||
" | ||
`; | ||
|
||
exports[`component Generator support angular 14 should create the component correctly but not export it in the entry point when it does not exist 1`] = ` | ||
"import { Component } from '@angular/core'; | ||
@Component({ | ||
selector: 'example', | ||
templateUrl: './example.component.html', | ||
styleUrls: ['./example.component.css'] | ||
}) | ||
export class ExampleComponent { | ||
} | ||
" | ||
`; |
53 changes: 53 additions & 0 deletions
53
packages/angular/src/generators/component/angular-v14/component.ts
This file contains 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,53 @@ | ||
import type { Tree } from '@nrwl/devkit'; | ||
import { | ||
formatFiles, | ||
normalizePath, | ||
readProjectConfiguration, | ||
readWorkspaceConfiguration, | ||
} from '@nrwl/devkit'; | ||
import { wrapAngularDevkitSchematic } from '@nrwl/devkit/ngcli-adapter'; | ||
import { pathStartsWith } from '../../utils/path'; | ||
import { exportComponentInEntryPoint } from './lib/component'; | ||
import { normalizeOptions } from './lib/normalize-options'; | ||
import type { NormalizedSchema, Schema } from './schema'; | ||
|
||
export async function componentGenerator(tree: Tree, rawOptions: Schema) { | ||
const options = await normalizeOptions(tree, rawOptions); | ||
const { projectSourceRoot, ...schematicOptions } = options; | ||
|
||
checkPathUnderProjectRoot(tree, options); | ||
|
||
const angularComponentSchematic = wrapAngularDevkitSchematic( | ||
'@schematics/angular', | ||
'component' | ||
); | ||
await angularComponentSchematic(tree, schematicOptions); | ||
|
||
exportComponentInEntryPoint(tree, options); | ||
|
||
await formatFiles(tree); | ||
} | ||
|
||
function checkPathUnderProjectRoot(tree: Tree, schema: NormalizedSchema): void { | ||
if (!schema.path) { | ||
return; | ||
} | ||
|
||
const project = | ||
schema.project ?? readWorkspaceConfiguration(tree).defaultProject; | ||
const { root } = readProjectConfiguration(tree, project); | ||
|
||
let pathToComponent = normalizePath(schema.path); | ||
pathToComponent = pathToComponent.startsWith('/') | ||
? pathToComponent.slice(1) | ||
: pathToComponent; | ||
|
||
if (!pathStartsWith(pathToComponent, root)) { | ||
throw new Error( | ||
`The path provided for the component (${schema.path}) does not exist under the project root (${root}). ` + | ||
`Please make sure to provide a path that exists under the project root.` | ||
); | ||
} | ||
} | ||
|
||
export default componentGenerator; |
59 changes: 59 additions & 0 deletions
59
packages/angular/src/generators/component/angular-v14/lib/component.ts
This file contains 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,59 @@ | ||
import type { Tree } from '@nrwl/devkit'; | ||
import { logger, readProjectConfiguration, stripIndents } from '@nrwl/devkit'; | ||
import { getComponentFileInfo } from '../../../utils/file-info'; | ||
import { locateLibraryEntryPointFromDirectory } from '../../../utils/entry-point'; | ||
import { getRelativeImportToFile } from '../../../utils/path'; | ||
import type { NormalizedSchema } from '../schema'; | ||
import { shouldExportInEntryPoint } from './entry-point'; | ||
import { findModuleFromOptions } from './module'; | ||
|
||
export function exportComponentInEntryPoint( | ||
tree: Tree, | ||
schema: NormalizedSchema | ||
): void { | ||
if (!schema.export || (schema.skipImport && !schema.standalone)) { | ||
return; | ||
} | ||
|
||
const { root, projectType } = readProjectConfiguration(tree, schema.project); | ||
|
||
if (projectType === 'application') { | ||
return; | ||
} | ||
|
||
const { directory, filePath } = getComponentFileInfo(tree, schema); | ||
|
||
const entryPointPath = locateLibraryEntryPointFromDirectory( | ||
tree, | ||
directory, | ||
root, | ||
schema.projectSourceRoot | ||
); | ||
if (!entryPointPath) { | ||
logger.warn( | ||
`Unable to determine whether the component should be exported in the library entry point file. ` + | ||
`The library's entry point file could not be found. Skipping exporting the component in the entry point file.` | ||
); | ||
|
||
return; | ||
} | ||
|
||
if (!schema.standalone) { | ||
const modulePath = findModuleFromOptions(tree, schema, root); | ||
if (!shouldExportInEntryPoint(tree, entryPointPath, modulePath)) { | ||
return; | ||
} | ||
} | ||
|
||
const relativePathFromEntryPoint = getRelativeImportToFile( | ||
entryPointPath, | ||
filePath | ||
); | ||
const updateEntryPointContent = stripIndents`${tree.read( | ||
entryPointPath, | ||
'utf-8' | ||
)} | ||
export * from "${relativePathFromEntryPoint}";`; | ||
|
||
tree.write(entryPointPath, updateEntryPointContent); | ||
} |
25 changes: 25 additions & 0 deletions
25
packages/angular/src/generators/component/angular-v14/lib/entry-point.ts
This file contains 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,25 @@ | ||
import type { Tree } from '@nrwl/devkit'; | ||
import { tsquery } from '@phenomnomnominal/tsquery'; | ||
import type { StringLiteral } from 'typescript'; | ||
import { getRelativeImportToFile } from '../../../utils/path'; | ||
|
||
export function shouldExportInEntryPoint( | ||
tree: Tree, | ||
entryPoint: string, | ||
modulePath: string | ||
): boolean { | ||
if (!modulePath) { | ||
return false; | ||
} | ||
|
||
const moduleImportPath = getRelativeImportToFile(entryPoint, modulePath); | ||
const entryPointContent = tree.read(entryPoint, 'utf-8'); | ||
const entryPointAst = tsquery.ast(entryPointContent); | ||
const moduleExport = tsquery( | ||
entryPointAst, | ||
`ExportDeclaration StringLiteral[value='${moduleImportPath}']`, | ||
{ visitAllChildren: true } | ||
)[0] as StringLiteral; | ||
|
||
return Boolean(moduleExport); | ||
} |
84 changes: 84 additions & 0 deletions
84
packages/angular/src/generators/component/angular-v14/lib/module.ts
This file contains 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,84 @@ | ||
import type { Tree } from '@nrwl/devkit'; | ||
import { joinPathFragments, normalizePath } from '@nrwl/devkit'; | ||
import { basename, dirname } from 'path'; | ||
import type { NormalizedSchema } from '../schema'; | ||
|
||
// Adapted from https://github.com/angular/angular-cli/blob/main/packages/schematics/angular/utility/find-module.ts#L29 | ||
// to match the logic in the component schematic. It doesn't throw if it can't | ||
// find a module since the schematic would have thrown before getting here. | ||
const moduleExt = '.module.ts'; | ||
const routingModuleExt = '-routing.module.ts'; | ||
|
||
export function findModuleFromOptions( | ||
tree: Tree, | ||
options: NormalizedSchema, | ||
projectRoot: string | ||
): string | null { | ||
if (!options.module) { | ||
const pathToCheck = joinPathFragments(options.path, options.name); | ||
|
||
return normalizePath(findModule(tree, pathToCheck, projectRoot)); | ||
} else { | ||
const modulePath = joinPathFragments(options.path, options.module); | ||
const componentPath = joinPathFragments(options.path, options.name); | ||
const moduleBaseName = basename(modulePath); | ||
|
||
const candidateSet = new Set<string>([options.path]); | ||
|
||
const projectRootParent = dirname(projectRoot); | ||
for (let dir = modulePath; dir !== projectRootParent; dir = dirname(dir)) { | ||
candidateSet.add(dir); | ||
} | ||
for (let dir = componentPath; dir !== projectRoot; dir = dirname(dir)) { | ||
candidateSet.add(dir); | ||
} | ||
|
||
const candidatesDirs = [...candidateSet].sort( | ||
(a, b) => b.length - a.length | ||
); | ||
for (const c of candidatesDirs) { | ||
const candidateFiles = [ | ||
'', | ||
`${moduleBaseName}.ts`, | ||
`${moduleBaseName}${moduleExt}`, | ||
].map((x) => joinPathFragments(c, x)); | ||
|
||
for (const sc of candidateFiles) { | ||
if (tree.isFile(sc)) { | ||
return normalizePath(sc); | ||
} | ||
} | ||
} | ||
|
||
return null; | ||
} | ||
} | ||
|
||
function findModule( | ||
tree: Tree, | ||
generateDir: string, | ||
projectRoot: string | ||
): string | null { | ||
let dir = generateDir; | ||
const projectRootParent = dirname(projectRoot); | ||
|
||
while (dir !== projectRootParent) { | ||
const allMatches = tree | ||
.children(dir) | ||
.map((path) => joinPathFragments(dir, path)) | ||
.filter((path) => tree.isFile(path) && path.endsWith(moduleExt)); | ||
const filteredMatches = allMatches.filter( | ||
(path) => !path.endsWith(routingModuleExt) | ||
); | ||
|
||
if (filteredMatches.length == 1) { | ||
return filteredMatches[0]; | ||
} else if (filteredMatches.length > 1) { | ||
return null; | ||
} | ||
|
||
dir = dirname(dir); | ||
} | ||
|
||
return null; | ||
} |
Oops, something went wrong.