From cc99f14a0a52fb30105127658f3537e6fddda957 Mon Sep 17 00:00:00 2001 From: erdemcaygor Date: Wed, 16 Apr 2025 00:16:31 +0300 Subject: [PATCH] schematics standalone updates --- .../src/commands/change-theme/index.ts | 48 +++--- .../src/commands/create-lib/index.ts | 93 ++++++++--- .../angular/add-declaration-to-ng-module.ts | 11 +- .../schematics/src/utils/angular/ast-utils.ts | 150 ++++++++++-------- .../schematics/src/utils/angular/change.ts | 20 ++- .../src/utils/angular/dependencies.ts | 2 +- .../src/utils/angular/dependency.ts | 10 +- .../src/utils/angular/find-module.ts | 12 +- .../src/utils/angular/generate-from-files.ts | 22 ++- .../schematics/src/utils/angular/index.ts | 1 + .../schematics/src/utils/angular/json-file.ts | 18 ++- .../src/utils/angular/ng-ast-utils.ts | 24 ++- .../src/utils/angular/parse-name.ts | 2 +- .../schematics/src/utils/angular/paths.ts | 12 +- .../src/utils/angular/project-targets.ts | 12 +- .../src/utils/angular/validation.ts | 5 +- .../src/utils/angular/workspace-models.ts | 20 ++- .../schematics/src/utils/angular/workspace.ts | 12 +- .../schematics/src/utils/workspace.ts | 14 +- 19 files changed, 315 insertions(+), 173 deletions(-) diff --git a/npm/ng-packs/packages/schematics/src/commands/change-theme/index.ts b/npm/ng-packs/packages/schematics/src/commands/change-theme/index.ts index 56e80a0762..a2f55cf2bb 100644 --- a/npm/ng-packs/packages/schematics/src/commands/change-theme/index.ts +++ b/npm/ng-packs/packages/schematics/src/commands/change-theme/index.ts @@ -5,17 +5,18 @@ import * as ts from 'typescript'; import { allStyles, importMap, styleMap } from './style-map'; import { ChangeThemeOptions } from './model'; import { - applicationHasStandaloneTemplate, + addRootImport, Change, createDefaultPath, + getWorkspace, InsertChange, isLibrary, + isStandaloneApp, updateWorkspace, WorkspaceDefinition, } from '../../utils'; import { ThemeOptionsEnum } from './theme-options.enum'; import { - addImportToModule, addProviderToModule, findNodes, getDecoratorMetadata, @@ -69,16 +70,20 @@ function updateProjectStyle( function updateAppModule(selectedProject: string, targetThemeName: ThemeOptionsEnum): Rule { return async (host: Tree) => { - const isStandalone = applicationHasStandaloneTemplate(host, selectedProject); + const workspace = await getWorkspace(host); + const project = workspace.projects.get(selectedProject); + const sourceRoot = project?.sourceRoot || 'src'; + const isStandalone = isStandaloneApp(host, `${sourceRoot}/main.ts`); + console.log('isStandalone --->>>>>', isStandalone); const defaultPath = await createDefaultPath(host, selectedProject); - const appModulePath = defaultPath + `${isStandalone ? '/app.component.ts' : '/app.module.ts'}`; + const appModulePath = + defaultPath + `${isStandalone ? `${sourceRoot}/main.ts` : '/app.module.ts'}`; return chain([ removeImportPath(appModulePath, targetThemeName), removeImportFromNgModuleMetadata(appModulePath, targetThemeName), removeProviderFromNgModuleMetadata(appModulePath, targetThemeName), - insertImports(appModulePath, targetThemeName, isStandalone), - insertProviders(appModulePath, targetThemeName), + insertImports(selectedProject, targetThemeName), ]); }; } @@ -195,35 +200,36 @@ export function removeProviderFromNgModuleMetadata( }; } -export function insertImports( - appModulePath: string, - selectedTheme: ThemeOptionsEnum, - isStandalone = false, -): Rule { +export function insertImports(projectName: string, selectedTheme: ThemeOptionsEnum): Rule { return (host: Tree) => { - const recorder = host.beginUpdate(appModulePath); - const source = createSourceFile(host, appModulePath); const selected = importMap.get(selectedTheme); if (!selected) { return host; } - const changes: Change[] = []; + const rules: Rule[] = []; - selected.map(({ importName, path }) => - changes.push(...addImportToModule(source, appModulePath, importName, path, isStandalone)), - ); + selected.map(({ importName, path }) => { + rules.push( + addRootImport(projectName, code => { + const configFn = code.external(importName, path); + console.log('configFn --->>>', configFn); + return code.code`${configFn}`; + }), + ); + }); - if (changes.length > 0) { + /* if (changes.length > 0) { for (const change of changes) { if (change instanceof InsertChange) { recorder.insertLeft(change.order, change.toAdd); } } } - host.commitUpdate(recorder); - return host; + host.commitUpdate(recorder);*/ + console.log(rules); + return chain(rules); }; } @@ -278,7 +284,7 @@ export function createSourceFile(host: Tree, appModulePath: string): ts.SourceFi * @param selectedTheme The selected theme * @param getAll If true, returns all import paths */ -export function getImportPaths(selectedTheme: ThemeOptionsEnum, getAll: boolean = false) { +export function getImportPaths(selectedTheme: ThemeOptionsEnum, getAll = false) { if (getAll) { return Array.from(importMap.values()).reduce((acc, val) => [...acc, ...val], []); } diff --git a/npm/ng-packs/packages/schematics/src/commands/create-lib/index.ts b/npm/ng-packs/packages/schematics/src/commands/create-lib/index.ts index 78a7d2e904..44d181a175 100644 --- a/npm/ng-packs/packages/schematics/src/commands/create-lib/index.ts +++ b/npm/ng-packs/packages/schematics/src/commands/create-lib/index.ts @@ -12,19 +12,19 @@ import * as ts from 'typescript'; import { join, normalize } from '@angular-devkit/core'; import { - addImportToModule, + addRootImport, + addRootProvider, addRouteDeclarationToModule, applyWithOverwrite, - camel, getFirstApplication, getWorkspace, InsertChange, interpolate, isLibrary, + isStandaloneApp, JSONFile, kebab, pascal, - readWorkspaceSchema, resolveProject, updateWorkspace, } from '../../utils'; @@ -221,15 +221,44 @@ export function importConfigModuleToDefaultProjectAppModule( options: GenerateLibSchema, ) { return (tree: Tree) => { - const projectName = readWorkspaceSchema(tree).defaultProject || getFirstApplication(tree).name!; + // const projectName = readWorkspaceSchema(tree).defaultProject || getFirstApplication(tree).name!; + const projectName = getFirstApplication(tree).name!; const project = workspace.projects.get(projectName); - const appModulePath = `${project?.sourceRoot}/app/app.module.ts`; - const appModule = tree.read(appModulePath); - if (!appModule) { - return; + const sourceRoot = project?.sourceRoot || 'src'; + + const isSourceStandalone = isStandaloneApp(tree, `${sourceRoot}/main.ts`); + console.log('isStandalone --->>>>', isSourceStandalone); + const rules: Rule[] = []; + + if (options.isStandaloneTemplate) { + rules.push( + addRootProvider(projectName, code => { + const configFn = code.external( + `provide${pascal(packageName)}Config`, + `${kebab(packageName)}/config`, + ); + return code.code`${configFn}()`; + }), + ); + } else { + rules.push( + addRootImport(projectName, code => { + const configFn = code.external( + `${pascal(packageName)}ConfigModule`, + `${kebab(packageName)}/config`, + ); + return code.code`${configFn}()`; + }), + ); } - const appModuleContent = appModule.toString(); - if ( + + // const appModulePath = `${project?.sourceRoot}/app/app.module.ts`; + // const appModule = tree.read(appModulePath); + // if (!appModule) { + // return; + // } + // const appModuleContent = appModule.toString(); + /* if ( appModuleContent.includes( options.isStandaloneTemplate ? `provide${pascal(packageName)}Config` @@ -237,9 +266,9 @@ export function importConfigModuleToDefaultProjectAppModule( ) ) { return; - } + }*/ - const rootConfigStatement = options.isStandaloneTemplate + /* const rootConfigStatement = options.isStandaloneTemplate ? `provide${pascal(packageName)}Config()` : `${pascal(packageName)}ConfigModule.forRoot()`; const text = tree.read(appModulePath); @@ -257,7 +286,6 @@ export function importConfigModuleToDefaultProjectAppModule( appModulePath, rootConfigStatement, `${kebab(packageName)}/config`, - options.isStandaloneTemplate, ); const recorder = tree.beginUpdate(appModulePath); for (const change of changes) { @@ -265,9 +293,9 @@ export function importConfigModuleToDefaultProjectAppModule( recorder.insertLeft(change.pos, change.toAdd); } } - tree.commitUpdate(recorder); + tree.commitUpdate(recorder);*/ - return; + return chain(rules); }; } @@ -275,17 +303,37 @@ export function addRoutingToAppRoutingModule( workspace: WorkspaceDefinition, packageName: string, options: GenerateLibSchema, -) { +): Rule { return (tree: Tree) => { - const projectName = readWorkspaceSchema(tree).defaultProject || getFirstApplication(tree).name!; + // const projectName = readWorkspaceSchema(tree).defaultProject || getFirstApplication(tree).name!; + const projectName = getFirstApplication(tree).name!; const project = workspace.projects.get(projectName); + const sourceRoot = project?.sourceRoot || 'src'; + + const mainPath = `${sourceRoot}/main.ts`; + const isSourceStandalone = isStandaloneApp(tree, mainPath); + const pascalName = pascal(packageName); + const routePath = `${kebab(packageName)}`; + + if (isSourceStandalone) { + return addRootProvider(projectName, code => { + const routeExpr = options.isStandaloneTemplate + ? `() => import('${routePath}/routes').then(m => m.${pascalName}Routes)` + : `() => import('${routePath}').then(m => m.${moduleName}.forLazy())`; + + return code.code`provideRouter([ + { path: '${routePath}', loadChildren: ${routeExpr} } + ])`; + }); + } + const appRoutingModulePath = `${project?.sourceRoot}/app/app-routing.module.ts`; const appRoutingModule = tree.read(appRoutingModulePath); if (!appRoutingModule) { return; } const appRoutingModuleContent = appRoutingModule.toString(); - const moduleName = `${pascal(packageName)}Module`; + const moduleName = `${pascalName}Module`; if (appRoutingModuleContent.includes(moduleName)) { return; } @@ -296,12 +344,11 @@ export function addRoutingToAppRoutingModule( ts.ScriptTarget.Latest, true, ); - const importPath = `${kebab(packageName)}`; const importStatement = options.isStandaloneTemplate - ? `() => import('${importPath}').then(m => m.${pascal(packageName)}Routes)` - : `() => import('${importPath}').then(m => m.${moduleName}.forLazy())`; - const routeDefinition = `{ path: '${kebab(packageName)}', loadChildren: ${importStatement} }`; - const change = addRouteDeclarationToModule(source, `${kebab(packageName)}`, routeDefinition); + ? `() => import('${routePath}').then(m => m.${pascalName}Routes)` + : `() => import('${routePath}').then(m => m.${moduleName}.forLazy())`; + const routeDefinition = `{ path: '${routePath}', loadChildren: ${importStatement} }`; + const change = addRouteDeclarationToModule(source, routePath, routeDefinition); const recorder = tree.beginUpdate(appRoutingModulePath); if (change instanceof InsertChange) { diff --git a/npm/ng-packs/packages/schematics/src/utils/angular/add-declaration-to-ng-module.ts b/npm/ng-packs/packages/schematics/src/utils/angular/add-declaration-to-ng-module.ts index f62e3922fd..6e5dc266b7 100644 --- a/npm/ng-packs/packages/schematics/src/utils/angular/add-declaration-to-ng-module.ts +++ b/npm/ng-packs/packages/schematics/src/utils/angular/add-declaration-to-ng-module.ts @@ -3,7 +3,7 @@ * 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.io/license + * found in the LICENSE file at https://angular.dev/license */ import { Rule, Tree, strings } from '@angular-devkit/schematics'; @@ -19,6 +19,7 @@ export interface DeclarationToNgModuleOptions { flat?: boolean; export?: boolean; type: string; + typeSeparator?: '.' | '-'; skipImport?: boolean; standalone?: boolean; } @@ -30,6 +31,8 @@ export function addDeclarationToNgModule(options: DeclarationToNgModuleOptions): return host; } + const typeSeparator = options.typeSeparator ?? '.'; + const sourceText = host.readText(modulePath); const source = ts.createSourceFile(modulePath, sourceText, ts.ScriptTarget.Latest, true); @@ -37,11 +40,11 @@ export function addDeclarationToNgModule(options: DeclarationToNgModuleOptions): `/${options.path}/` + (options.flat ? '' : strings.dasherize(options.name) + '/') + strings.dasherize(options.name) + - (options.type ? '.' : '') + - strings.dasherize(options.type); + (options.type ? typeSeparator + strings.dasherize(options.type) : ''); const importPath = buildRelativePath(modulePath, filePath); - const classifiedName = strings.classify(options.name) + strings.classify(options.type); + const classifiedName = + strings.classify(options.name) + (options.type ? strings.classify(options.type) : ''); const changes = addDeclarationToModule(source, modulePath, classifiedName, importPath); if (options.export) { diff --git a/npm/ng-packs/packages/schematics/src/utils/angular/ast-utils.ts b/npm/ng-packs/packages/schematics/src/utils/angular/ast-utils.ts index 97eaa2e630..2683e20149 100644 --- a/npm/ng-packs/packages/schematics/src/utils/angular/ast-utils.ts +++ b/npm/ng-packs/packages/schematics/src/utils/angular/ast-utils.ts @@ -3,20 +3,22 @@ * 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.io/license + * found in the LICENSE file at https://angular.dev/license */ import { tags } from '@angular-devkit/core'; import * as ts from 'typescript'; import { Change, InsertChange, NoopChange } from './change'; +import { getEOL } from './eol'; /** * Add Import `import { symbolName } from fileName` if the import doesn't exit * already. Assumes fileToEdit can be resolved and accessed. - * @param fileToEdit (file we want to add import to) - * @param symbolName (item to import) - * @param fileName (path to the file) - * @param isDefault (if true, import follows style for importing default exports) + * @param fileToEdit File we want to add import to. + * @param symbolName Item to import. + * @param fileName Path to the file. + * @param isDefault If true, import follows style for importing default exports. + * @param alias Alias that the symbol should be inserted under. * @return Change */ export function insertImport( @@ -25,46 +27,40 @@ export function insertImport( symbolName: string, fileName: string, isDefault = false, + alias?: string, ): Change { const rootNode = source; - const allImports = findNodes(rootNode, ts.SyntaxKind.ImportDeclaration); + const allImports = findNodes(rootNode, ts.isImportDeclaration); + const importExpression = alias ? `${symbolName} as ${alias}` : symbolName; // get nodes that map to import statements from the file fileName const relevantImports = allImports.filter(node => { - // StringLiteral of the ImportDeclaration is the import file (fileName in this case). - const importFiles = node - .getChildren() - .filter(ts.isStringLiteral) - .map(n => n.text); - - return importFiles.filter(file => file === fileName).length === 1; + return ts.isStringLiteralLike(node.moduleSpecifier) && node.moduleSpecifier.text === fileName; }); if (relevantImports.length > 0) { - let importsAsterisk = false; - // imports from import file - const imports: ts.Node[] = []; - relevantImports.forEach(n => { - Array.prototype.push.apply(imports, findNodes(n, ts.SyntaxKind.Identifier)); - if (findNodes(n, ts.SyntaxKind.AsteriskToken).length > 0) { - importsAsterisk = true; - } + const hasNamespaceImport = relevantImports.some(node => { + return node.importClause?.namedBindings?.kind === ts.SyntaxKind.NamespaceImport; }); // if imports * from fileName, don't add symbolName - if (importsAsterisk) { + if (hasNamespaceImport) { return new NoopChange(); } - const importTextNodes = imports.filter(n => (n as ts.Identifier).text === symbolName); + const imports = relevantImports.flatMap(node => { + return node.importClause?.namedBindings && ts.isNamedImports(node.importClause.namedBindings) + ? node.importClause.namedBindings.elements + : []; + }); // insert import if it's not there - if (importTextNodes.length === 0) { + if (!imports.some(node => (node.propertyName || node.name).text === symbolName)) { const fallbackPos = findNodes(relevantImports[0], ts.SyntaxKind.CloseBraceToken)[0].getStart() || findNodes(relevantImports[0], ts.SyntaxKind.FromKeyword)[0].getStart(); - return insertAfterLastOccurrence(imports, `, ${symbolName}`, fileToEdit, fallbackPos); + return insertAfterLastOccurrence(imports, `, ${importExpression}`, fileToEdit, fallbackPos); } return new NoopChange(); @@ -78,12 +74,13 @@ export function insertImport( } const open = isDefault ? '' : '{ '; const close = isDefault ? '' : ' }'; + const eol = getEOL(rootNode.getText()); // if there are no imports or 'use strict' statement, insert import at beginning of file const insertAtBeginning = allImports.length === 0 && useStrict.length === 0; - const separator = insertAtBeginning ? '' : ';\n'; + const separator = insertAtBeginning ? '' : `;${eol}`; const toInsert = - `${separator}import ${open}${symbolName}${close}` + - ` from '${fileName}'${insertAtBeginning ? ';\n' : ''}`; + `${separator}import ${open}${importExpression}${close}` + + ` from '${fileName}'${insertAtBeginning ? `;${eol}` : ''}`; return insertAfterLastOccurrence( allImports, @@ -222,7 +219,7 @@ function nodesByPosition(first: ts.Node, second: ts.Node): number { * @throw Error if toInsert is first occurence but fall back is not set */ export function insertAfterLastOccurrence( - nodes: ts.Node[], + nodes: ts.Node[] | ts.NodeArray, toInsert: string, file: string, fallbackPos: number, @@ -345,7 +342,7 @@ export function getDecoratorMetadata( export function getMetadataField( node: ts.ObjectLiteralExpression, metadataField: string, -): ts.ObjectLiteralElement[] { +): ts.PropertyAssignment[] { return ( node.properties .filter(ts.isPropertyAssignment) @@ -399,12 +396,7 @@ export function addSymbolToNgModuleMetadata( if (importPath !== null) { return [ new InsertChange(ngModulePath, position, toInsert), - insertImport( - source, - ngModulePath, - symbolName.replace(/\..*$/, '').replace(/\(\)/, ''), - importPath, - ), + insertImport(source, ngModulePath, symbolName.replace(/\..*$/, ''), importPath), ]; } else { return [new InsertChange(ngModulePath, position, toInsert)]; @@ -420,7 +412,7 @@ export function addSymbolToNgModuleMetadata( return []; } - let expresssion: ts.Expression | ts.ArrayLiteralExpression; + let expression: ts.Expression | ts.ArrayLiteralExpression; const assignmentInit = assignment.initializer; const elements = assignmentInit.elements; @@ -430,20 +422,20 @@ export function addSymbolToNgModuleMetadata( return []; } - expresssion = elements[elements.length - 1]; + expression = elements[elements.length - 1]; } else { - expresssion = assignmentInit; + expression = assignmentInit; } let toInsert: string; - let position = expresssion.getEnd(); - if (ts.isArrayLiteralExpression(expresssion)) { + let position = expression.getEnd(); + if (ts.isArrayLiteralExpression(expression)) { // We found the field but it's empty. Insert it just before the `]`. position--; toInsert = `\n${tags.indentBy(4)`${symbolName}`}\n `; } else { // Get the indentation of the last element, if any. - const text = expresssion.getFullText(source); + const text = expression.getFullText(source); const matches = text.match(/^(\r?\n)(\s*)/); if (matches) { toInsert = `,${matches[1]}${tags.indentBy(matches[2].length)`${symbolName}`}`; @@ -451,15 +443,11 @@ export function addSymbolToNgModuleMetadata( toInsert = `, ${symbolName}`; } } + if (importPath !== null) { return [ new InsertChange(ngModulePath, position, toInsert), - insertImport( - source, - ngModulePath, - symbolName.replace(/\..*$/, '').replace(/\(\)/, ''), - importPath, - ), + insertImport(source, ngModulePath, symbolName.replace(/\..*$/, ''), importPath), ]; } @@ -493,15 +481,8 @@ export function addImportToModule( modulePath: string, classifiedName: string, importPath: string, - isStandalone = false, ): Change[] { - return addSymbolToNgModuleMetadata( - source, - modulePath, - isStandalone ? 'providers' : 'imports', - classifiedName, - importPath, - ); + return addSymbolToNgModuleMetadata(source, modulePath, 'imports', classifiedName, importPath); } /** @@ -579,13 +560,9 @@ export function getRouterModuleDeclaration(source: ts.SourceFile): ts.Expression } const matchingProperties = getMetadataField(node, 'imports'); - if (!matchingProperties) { - return; - } - - const assignment = matchingProperties[0] as ts.PropertyAssignment; + const assignment = matchingProperties[0]; - if (assignment.initializer.kind !== ts.SyntaxKind.ArrayLiteralExpression) { + if (!assignment || assignment.initializer.kind !== ts.SyntaxKind.ArrayLiteralExpression) { return; } @@ -685,3 +662,52 @@ export function addRouteDeclarationToModule( return new InsertChange(fileToAdd, insertPos, route); } + +/** Asserts if the specified node is a named declaration (e.g. class, interface). */ +function isNamedNode( + node: ts.Node & { name?: ts.Node }, +): node is ts.Node & { name: ts.Identifier } { + return !!node.name && ts.isIdentifier(node.name); +} + +/** + * Determines if a SourceFile has a top-level declaration whose name matches a specific symbol. + * Can be used to avoid conflicts when inserting new imports into a file. + * @param sourceFile File in which to search. + * @param symbolName Name of the symbol to search for. + * @param skipModule Path of the module that the symbol may have been imported from. Used to + * avoid false positives where the same symbol we're looking for may have been imported. + */ +export function hasTopLevelIdentifier( + sourceFile: ts.SourceFile, + symbolName: string, + skipModule: string | null = null, +): boolean { + for (const node of sourceFile.statements) { + if (isNamedNode(node) && node.name.text === symbolName) { + return true; + } + + if ( + ts.isVariableStatement(node) && + node.declarationList.declarations.some(decl => { + return isNamedNode(decl) && decl.name.text === symbolName; + }) + ) { + return true; + } + + if ( + ts.isImportDeclaration(node) && + ts.isStringLiteralLike(node.moduleSpecifier) && + node.moduleSpecifier.text !== skipModule && + node.importClause?.namedBindings && + ts.isNamedImports(node.importClause.namedBindings) && + node.importClause.namedBindings.elements.some(el => el.name.text === symbolName) + ) { + return true; + } + } + + return false; +} diff --git a/npm/ng-packs/packages/schematics/src/utils/angular/change.ts b/npm/ng-packs/packages/schematics/src/utils/angular/change.ts index 08df56a6dd..62e3ab659a 100644 --- a/npm/ng-packs/packages/schematics/src/utils/angular/change.ts +++ b/npm/ng-packs/packages/schematics/src/utils/angular/change.ts @@ -3,7 +3,7 @@ * 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.io/license + * found in the LICENSE file at https://angular.dev/license */ import { UpdateRecorder } from '@angular-devkit/schematics'; @@ -47,7 +47,11 @@ export class InsertChange implements Change { order: number; description: string; - constructor(public path: string, public pos: number, public toAdd: string) { + constructor( + public path: string, + public pos: number, + public toAdd: string, + ) { if (pos < 0) { throw new Error('Negative positions are invalid'); } @@ -59,7 +63,7 @@ export class InsertChange implements Change { * This method does not insert spaces if there is none in the original string. */ apply(host: Host) { - return host.read(this.path).then((content) => { + return host.read(this.path).then(content => { const prefix = content.substring(0, this.pos); const suffix = content.substring(this.pos); @@ -75,7 +79,11 @@ export class RemoveChange implements Change { order: number; description: string; - constructor(public path: string, private pos: number, public toRemove: string) { + constructor( + public path: string, + private pos: number, + public toRemove: string, + ) { if (pos < 0) { throw new Error('Negative positions are invalid'); } @@ -84,7 +92,7 @@ export class RemoveChange implements Change { } apply(host: Host): Promise { - return host.read(this.path).then((content) => { + return host.read(this.path).then(content => { const prefix = content.substring(0, this.pos); const suffix = content.substring(this.pos + this.toRemove.length); @@ -115,7 +123,7 @@ export class ReplaceChange implements Change { } apply(host: Host): Promise { - return host.read(this.path).then((content) => { + return host.read(this.path).then(content => { const prefix = content.substring(0, this.pos); const suffix = content.substring(this.pos + this.oldText.length); const text = content.substring(this.pos, this.pos + this.oldText.length); diff --git a/npm/ng-packs/packages/schematics/src/utils/angular/dependencies.ts b/npm/ng-packs/packages/schematics/src/utils/angular/dependencies.ts index c9aa617191..06c4f38653 100644 --- a/npm/ng-packs/packages/schematics/src/utils/angular/dependencies.ts +++ b/npm/ng-packs/packages/schematics/src/utils/angular/dependencies.ts @@ -3,7 +3,7 @@ * 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.io/license + * found in the LICENSE file at https://angular.dev/license */ import { Tree } from '@angular-devkit/schematics'; diff --git a/npm/ng-packs/packages/schematics/src/utils/angular/dependency.ts b/npm/ng-packs/packages/schematics/src/utils/angular/dependency.ts index 3522b59042..2f3501c33a 100644 --- a/npm/ng-packs/packages/schematics/src/utils/angular/dependency.ts +++ b/npm/ng-packs/packages/schematics/src/utils/angular/dependency.ts @@ -3,12 +3,12 @@ * 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.io/license + * found in the LICENSE file at https://angular.dev/license */ import { Rule, SchematicContext } from '@angular-devkit/schematics'; import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks'; -import * as path from 'path'; +import * as path from 'node:path'; const installTasks = new WeakMap>(); @@ -41,11 +41,13 @@ export enum InstallBehavior { * which may install the dependency. */ None, + /** * Automatically determine the need to schedule a {@link NodePackageInstallTask} based on * previous usage of the {@link addDependency} within the schematic. */ Auto, + /** * Always schedule a {@link NodePackageInstallTask} when the rule is executed. */ @@ -62,6 +64,7 @@ export enum ExistingBehavior { * The dependency will not be added or otherwise changed if it already exists. */ Skip, + /** * The dependency's existing specifier will be replaced with the specifier provided in the * {@link addDependency} call. A warning will also be shown during schematic execution to @@ -95,17 +98,20 @@ export function addDependency( * dependency will be added. Defaults to {@link DependencyType.Default} (`dependencies`). */ type?: DependencyType; + /** * The path of the package manifest file (`package.json`) that will be modified. * Defaults to `/package.json`. */ packageJsonPath?: string; + /** * The dependency installation behavior to use to determine whether a * {@link NodePackageInstallTask} should be scheduled after adding the dependency. * Defaults to {@link InstallBehavior.Auto}. */ install?: InstallBehavior; + /** * The behavior to use when the dependency already exists within the `package.json`. * Defaults to {@link ExistingBehavior.Replace}. diff --git a/npm/ng-packs/packages/schematics/src/utils/angular/find-module.ts b/npm/ng-packs/packages/schematics/src/utils/angular/find-module.ts index 112269d5e5..085592abaa 100644 --- a/npm/ng-packs/packages/schematics/src/utils/angular/find-module.ts +++ b/npm/ng-packs/packages/schematics/src/utils/angular/find-module.ts @@ -3,7 +3,7 @@ * 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.io/license + * found in the LICENSE file at https://angular.dev/license */ import { NormalizedRoot, Path, dirname, join, normalize, relative } from '@angular-devkit/core'; @@ -54,12 +54,12 @@ export function findModuleFromOptions(host: Tree, options: ModuleOptions): Path const candidatesDirs = [...candidateSet].sort((a, b) => b.length - a.length); for (const c of candidatesDirs) { - const candidateFiles = ['', `${moduleBaseName}.ts`, `${moduleBaseName}${moduleExt}`].map( - (x) => join(c, x), + const candidateFiles = ['', `${moduleBaseName}.ts`, `${moduleBaseName}${moduleExt}`].map(x => + join(c, x), ); for (const sc of candidateFiles) { - if (host.exists(sc)) { + if (host.exists(sc) && host.readText(sc).includes('@NgModule')) { return normalize(sc); } } @@ -85,8 +85,8 @@ export function findModule( let foundRoutingModule = false; while (dir) { - const allMatches = dir.subfiles.filter((p) => p.endsWith(moduleExt)); - const filteredMatches = allMatches.filter((p) => !p.endsWith(routingModuleExt)); + const allMatches = dir.subfiles.filter(p => p.endsWith(moduleExt)); + const filteredMatches = allMatches.filter(p => !p.endsWith(routingModuleExt)); foundRoutingModule = foundRoutingModule || allMatches.length !== filteredMatches.length; diff --git a/npm/ng-packs/packages/schematics/src/utils/angular/generate-from-files.ts b/npm/ng-packs/packages/schematics/src/utils/angular/generate-from-files.ts index 169c6c389a..946c4119d3 100644 --- a/npm/ng-packs/packages/schematics/src/utils/angular/generate-from-files.ts +++ b/npm/ng-packs/packages/schematics/src/utils/angular/generate-from-files.ts @@ -3,16 +3,18 @@ * 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.io/license + * found in the LICENSE file at https://angular.dev/license */ import { + FileOperator, Rule, Tree, apply, applyTemplates, chain, filter, + forEach, mergeWith, move, noop, @@ -30,6 +32,8 @@ export interface GenerateFromFilesOptions { prefix?: string; project: string; skipTests?: boolean; + templateFilesDirectory?: string; + type?: string; } export function generateFromFiles( @@ -41,19 +45,33 @@ export function generateFromFiles( options.prefix ??= ''; options.flat ??= true; + // Schematic templates require a defined type value + options.type ??= ''; + const parsedPath = parseName(options.path, options.name); options.name = parsedPath.name; options.path = parsedPath.path; validateClassName(strings.classify(options.name)); - const templateSource = apply(url('./files'), [ + const templateFilesDirectory = options.templateFilesDirectory ?? './files'; + const templateSource = apply(url(templateFilesDirectory), [ options.skipTests ? filter(path => !path.endsWith('.spec.ts.template')) : noop(), applyTemplates({ ...strings, ...options, ...extraTemplateValues, }), + !options.type + ? forEach((file => { + return file.path.includes('..') + ? { + content: file.content, + path: file.path.replace('..', '.'), + } + : file; + }) as FileOperator) + : noop(), move(parsedPath.path + (options.flat ? '' : '/' + strings.dasherize(options.name))), ]); diff --git a/npm/ng-packs/packages/schematics/src/utils/angular/index.ts b/npm/ng-packs/packages/schematics/src/utils/angular/index.ts index abf2992d90..2c1bfd086e 100644 --- a/npm/ng-packs/packages/schematics/src/utils/angular/index.ts +++ b/npm/ng-packs/packages/schematics/src/utils/angular/index.ts @@ -11,3 +11,4 @@ export * from './project-targets'; export * from './validation'; export * from './workspace'; export * from './workspace-models'; +export * from './standalone'; diff --git a/npm/ng-packs/packages/schematics/src/utils/angular/json-file.ts b/npm/ng-packs/packages/schematics/src/utils/angular/json-file.ts index 6bb532416f..26a06e16a6 100644 --- a/npm/ng-packs/packages/schematics/src/utils/angular/json-file.ts +++ b/npm/ng-packs/packages/schematics/src/utils/angular/json-file.ts @@ -3,7 +3,7 @@ * 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.io/license + * found in the LICENSE file at https://angular.dev/license */ import { JsonValue } from '@angular-devkit/core'; @@ -18,16 +18,22 @@ import { parseTree, printParseErrorCode, } from 'jsonc-parser'; +import { getEOL } from './eol'; export type InsertionIndex = (properties: string[]) => number; export type JSONPath = (string | number)[]; -/** @internal */ +/** @private */ export class JSONFile { content: string; + private eol: string; - constructor(private readonly host: Tree, private readonly path: string) { + constructor( + private readonly host: Tree, + private readonly path: string, + ) { this.content = this.host.readText(this.path); + this.eol = getEOL(this.content); } private _jsonAst: Node | undefined; @@ -73,15 +79,17 @@ export class JSONFile { let getInsertionIndex: InsertionIndex | undefined; if (insertInOrder === undefined) { const property = jsonPath.slice(-1)[0]; - getInsertionIndex = (properties) => - [...properties, property].sort().findIndex((p) => p === property); + getInsertionIndex = properties => + [...properties, property].sort().findIndex(p => p === property); } else if (insertInOrder !== false) { getInsertionIndex = insertInOrder; } const edits = modify(this.content, jsonPath, value, { getInsertionIndex, + formattingOptions: { + eol: this.eol, insertSpaces: true, tabSize: 2, }, diff --git a/npm/ng-packs/packages/schematics/src/utils/angular/ng-ast-utils.ts b/npm/ng-packs/packages/schematics/src/utils/angular/ng-ast-utils.ts index 75962f31d7..b4eb1fd056 100644 --- a/npm/ng-packs/packages/schematics/src/utils/angular/ng-ast-utils.ts +++ b/npm/ng-packs/packages/schematics/src/utils/angular/ng-ast-utils.ts @@ -3,14 +3,14 @@ * 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.io/license + * found in the LICENSE file at https://angular.dev/license */ -import { normalize } from '@angular-devkit/core'; import { SchematicsException, Tree } from '@angular-devkit/schematics'; -import { dirname } from 'path'; +import { dirname, join } from 'node:path/posix'; import * as ts from 'typescript'; import { findNode, getSourceNodes } from './ast-utils'; +import { findBootstrapApplicationCall } from './standalone/util'; export function findBootstrapModuleCall(host: Tree, mainPath: string): ts.CallExpression | null { const mainText = host.readText(mainPath); @@ -46,7 +46,7 @@ export function findBootstrapModuleCall(host: Tree, mainPath: string): ts.CallEx return bootstrapCall; } -export function findBootstrapModulePath(host: Tree, mainPath: string): string { +function findBootstrapModulePath(host: Tree, mainPath: string): string { const bootstrapCall = findBootstrapModuleCall(host, mainPath); if (!bootstrapCall) { throw new SchematicsException('Bootstrap call not found'); @@ -74,7 +74,21 @@ export function findBootstrapModulePath(host: Tree, mainPath: string): string { export function getAppModulePath(host: Tree, mainPath: string): string { const moduleRelativePath = findBootstrapModulePath(host, mainPath); const mainDir = dirname(mainPath); - const modulePath = normalize(`/${mainDir}/${moduleRelativePath}.ts`); + const modulePath = join(mainDir, `${moduleRelativePath}.ts`); return modulePath; } + +export function isStandaloneApp(host: Tree, mainPath: string): boolean { + try { + findBootstrapApplicationCall(host, mainPath); + + return true; + } catch (error) { + if (error instanceof SchematicsException) { + return false; + } + + throw error; + } +} diff --git a/npm/ng-packs/packages/schematics/src/utils/angular/parse-name.ts b/npm/ng-packs/packages/schematics/src/utils/angular/parse-name.ts index 01227198c9..37a019e940 100644 --- a/npm/ng-packs/packages/schematics/src/utils/angular/parse-name.ts +++ b/npm/ng-packs/packages/schematics/src/utils/angular/parse-name.ts @@ -3,7 +3,7 @@ * 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.io/license + * found in the LICENSE file at https://angular.dev/license */ import { Path, basename, dirname, join, normalize } from '@angular-devkit/core'; diff --git a/npm/ng-packs/packages/schematics/src/utils/angular/paths.ts b/npm/ng-packs/packages/schematics/src/utils/angular/paths.ts index dffa9e841d..2842018802 100644 --- a/npm/ng-packs/packages/schematics/src/utils/angular/paths.ts +++ b/npm/ng-packs/packages/schematics/src/utils/angular/paths.ts @@ -3,17 +3,15 @@ * 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.io/license + * found in the LICENSE file at https://angular.dev/license */ -import { normalize, split } from '@angular-devkit/core'; +import { join, relative } from 'node:path/posix'; export function relativePathToWorkspaceRoot(projectRoot: string | undefined): string { - const normalizedPath = split(normalize(projectRoot || '')); - - if (normalizedPath.length === 0 || !normalizedPath[0]) { + if (!projectRoot) { return '.'; - } else { - return normalizedPath.map(() => '..').join('/'); } + + return relative(join('/', projectRoot), '/') || '.'; } diff --git a/npm/ng-packs/packages/schematics/src/utils/angular/project-targets.ts b/npm/ng-packs/packages/schematics/src/utils/angular/project-targets.ts index 7f4b7ba8c6..8897a3ddab 100644 --- a/npm/ng-packs/packages/schematics/src/utils/angular/project-targets.ts +++ b/npm/ng-packs/packages/schematics/src/utils/angular/project-targets.ts @@ -3,11 +3,21 @@ * 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.io/license + * found in the LICENSE file at https://angular.dev/license */ import { SchematicsException } from '@angular-devkit/schematics'; +import { ProjectDefinition } from './workspace'; +import { Builders } from './workspace-models'; export function targetBuildNotFoundError(): SchematicsException { return new SchematicsException(`Project target "build" not found.`); } + +export function isUsingApplicationBuilder(project: ProjectDefinition): boolean { + const buildBuilder = project.targets.get('build')?.builder; + const isUsingApplicationBuilder = + buildBuilder === Builders.Application || buildBuilder === Builders.BuildApplication; + + return isUsingApplicationBuilder; +} diff --git a/npm/ng-packs/packages/schematics/src/utils/angular/validation.ts b/npm/ng-packs/packages/schematics/src/utils/angular/validation.ts index 619fe8e924..8b380d1b82 100644 --- a/npm/ng-packs/packages/schematics/src/utils/angular/validation.ts +++ b/npm/ng-packs/packages/schematics/src/utils/angular/validation.ts @@ -3,14 +3,15 @@ * 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.io/license + * found in the LICENSE file at https://angular.dev/license */ import { SchematicsException } from '@angular-devkit/schematics'; // Must start with a letter, and must contain only alphanumeric characters or dashes. // When adding a dash the segment after the dash must also start with a letter. -export const htmlSelectorRe = /^[a-zA-Z][.0-9a-zA-Z]*(:?-[a-zA-Z][.0-9a-zA-Z]*)*$/; +export const htmlSelectorRe = + /^[a-zA-Z][.0-9a-zA-Z]*((:?-[0-9]+)*|(:?-[a-zA-Z][.0-9a-zA-Z]*(:?-[0-9]+)*)*)$/; // See: https://github.com/tc39/proposal-regexp-unicode-property-escapes/blob/fe6d07fad74cd0192d154966baa1e95e7cda78a1/README.md#other-examples const ecmaIdentifierNameRegExp = /^(?:[$_\p{ID_Start}])(?:[$_\u200C\u200D\p{ID_Continue}])*$/u; diff --git a/npm/ng-packs/packages/schematics/src/utils/angular/workspace-models.ts b/npm/ng-packs/packages/schematics/src/utils/angular/workspace-models.ts index fb7f18c3f7..34c329b470 100644 --- a/npm/ng-packs/packages/schematics/src/utils/angular/workspace-models.ts +++ b/npm/ng-packs/packages/schematics/src/utils/angular/workspace-models.ts @@ -3,7 +3,7 @@ * 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.io/license + * found in the LICENSE file at https://angular.dev/license */ export enum ProjectType { @@ -18,16 +18,24 @@ export enum ProjectType { * `angular.json` workspace file. */ export enum Builders { + Application = '@angular-devkit/build-angular:application', AppShell = '@angular-devkit/build-angular:app-shell', Server = '@angular-devkit/build-angular:server', Browser = '@angular-devkit/build-angular:browser', + SsrDevServer = '@angular-devkit/build-angular:ssr-dev-server', + Prerender = '@angular-devkit/build-angular:prerender', + BrowserEsbuild = '@angular-devkit/build-angular:browser-esbuild', Karma = '@angular-devkit/build-angular:karma', + BuildKarma = '@angular/build:karma', TsLint = '@angular-devkit/build-angular:tslint', - DeprecatedNgPackagr = '@angular-devkit/build-ng-packagr:build', NgPackagr = '@angular-devkit/build-angular:ng-packagr', + BuildNgPackagr = '@angular/build:ng-packagr', DevServer = '@angular-devkit/build-angular:dev-server', + BuildDevServer = '@angular/build:dev-server', ExtractI18n = '@angular-devkit/build-angular:extract-i18n', - Protractor = '@angular-devkit/build-angular:protractor', + BuildExtractI18n = '@angular/build:extract-i18n', + Protractor = '@angular-devkit/build-angular:private-protractor', + BuildApplication = '@angular/build:application', } export interface FileReplacements { @@ -70,8 +78,9 @@ export interface BrowserBuilderOptions extends BrowserBuilderBaseOptions { } export interface ServeBuilderOptions { - browserTarget: string; + buildTarget: string; } + export interface LibraryBuilderOptions { tsConfig: string; project: string; @@ -138,11 +147,9 @@ export type E2EBuilderTarget = BuilderTarget; interface WorkspaceCLISchema { warnings?: Record; schematicCollections?: string[]; - defaultCollection?: string; } export interface WorkspaceSchema { version: 1; - defaultProject?: string; cli?: WorkspaceCLISchema; projects: { [key: string]: WorkspaceProject; @@ -165,6 +172,7 @@ export interface WorkspaceProject; + /** * Tool options. */ diff --git a/npm/ng-packs/packages/schematics/src/utils/angular/workspace.ts b/npm/ng-packs/packages/schematics/src/utils/angular/workspace.ts index 410c079e2f..b831458edf 100644 --- a/npm/ng-packs/packages/schematics/src/utils/angular/workspace.ts +++ b/npm/ng-packs/packages/schematics/src/utils/angular/workspace.ts @@ -3,7 +3,7 @@ * 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.io/license + * found in the LICENSE file at https://angular.dev/license */ import { json, workspaces } from '@angular-devkit/core'; @@ -20,7 +20,7 @@ export type TargetDefinition = workspaces.TargetDefinition; /** * A {@link workspaces.WorkspaceHost} backed by a Schematics {@link Tree} instance. */ -class TreeWorkspaceHost implements workspaces.WorkspaceHost { +export class TreeWorkspaceHost implements workspaces.WorkspaceHost { constructor(private readonly tree: Tree) {} async readFile(path: string): Promise { @@ -58,14 +58,12 @@ class TreeWorkspaceHost implements workspaces.WorkspaceHost { export function updateWorkspace( updater: (workspace: WorkspaceDefinition) => void | Rule | PromiseLike, ): Rule { - return async (tree: Tree) => { - const host = new TreeWorkspaceHost(tree); - - const { workspace } = await workspaces.readWorkspace(DEFAULT_WORKSPACE_PATH, host); + return async (host: Tree) => { + const workspace = await getWorkspace(host); const result = await updater(workspace); - await workspaces.writeWorkspace(workspace, host); + await workspaces.writeWorkspace(workspace, new TreeWorkspaceHost(host)); return result || noop; }; diff --git a/npm/ng-packs/packages/schematics/src/utils/workspace.ts b/npm/ng-packs/packages/schematics/src/utils/workspace.ts index 6218bc038e..9c71d31864 100644 --- a/npm/ng-packs/packages/schematics/src/utils/workspace.ts +++ b/npm/ng-packs/packages/schematics/src/utils/workspace.ts @@ -13,17 +13,6 @@ export function isLibrary(project: workspaces.ProjectDefinition): boolean { return project.extensions['projectType'] === ProjectType.Library; } -export function applicationHasStandaloneTemplate(tree: Tree, selectedProject?: string): boolean { - const workspace = readWorkspaceSchema(tree); - const project = workspace.projects[selectedProject ?? 0]; - - const mainPath = project.sourceRoot + '/main.ts'; - const mainSource = readFileInTree(tree, mainPath); - const mainContent = mainSource.toString(); - - return mainContent.includes('bootstrapComponent'); -} - export function readEnvironment(tree: Tree, project: workspaces.ProjectDefinition) { if (isLibrary(project)) return undefined; @@ -63,7 +52,8 @@ export async function resolveProject( // @typescript-eslint/no-explicit-any notFoundValue: T = NOT_FOUND_VALUE as unknown as any, ): Promise { - name = name || readWorkspaceSchema(tree).defaultProject || getFirstApplication(tree).name!; + // name = name || readWorkspaceSchema(tree).defaultProject || getFirstApplication(tree).name!; + name = name || getFirstApplication(tree).name!; const workspace = await getWorkspace(tree); let definition: Project['definition'] | undefined;