Browse Source

schematics standalone updates

pull/22645/head
erdemcaygor 1 year ago
parent
commit
cc99f14a0a
  1. 48
      npm/ng-packs/packages/schematics/src/commands/change-theme/index.ts
  2. 93
      npm/ng-packs/packages/schematics/src/commands/create-lib/index.ts
  3. 11
      npm/ng-packs/packages/schematics/src/utils/angular/add-declaration-to-ng-module.ts
  4. 150
      npm/ng-packs/packages/schematics/src/utils/angular/ast-utils.ts
  5. 20
      npm/ng-packs/packages/schematics/src/utils/angular/change.ts
  6. 2
      npm/ng-packs/packages/schematics/src/utils/angular/dependencies.ts
  7. 10
      npm/ng-packs/packages/schematics/src/utils/angular/dependency.ts
  8. 12
      npm/ng-packs/packages/schematics/src/utils/angular/find-module.ts
  9. 22
      npm/ng-packs/packages/schematics/src/utils/angular/generate-from-files.ts
  10. 1
      npm/ng-packs/packages/schematics/src/utils/angular/index.ts
  11. 18
      npm/ng-packs/packages/schematics/src/utils/angular/json-file.ts
  12. 24
      npm/ng-packs/packages/schematics/src/utils/angular/ng-ast-utils.ts
  13. 2
      npm/ng-packs/packages/schematics/src/utils/angular/parse-name.ts
  14. 12
      npm/ng-packs/packages/schematics/src/utils/angular/paths.ts
  15. 12
      npm/ng-packs/packages/schematics/src/utils/angular/project-targets.ts
  16. 5
      npm/ng-packs/packages/schematics/src/utils/angular/validation.ts
  17. 20
      npm/ng-packs/packages/schematics/src/utils/angular/workspace-models.ts
  18. 12
      npm/ng-packs/packages/schematics/src/utils/angular/workspace.ts
  19. 14
      npm/ng-packs/packages/schematics/src/utils/workspace.ts

48
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], []);
}

93
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) {

11
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) {

150
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<ts.Node>,
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;
}

20
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<void> {
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<void> {
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);

2
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';

10
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<SchematicContext, Set<string>>();
@ -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}.

12
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;

22
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))),
]);

1
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';

18
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,
},

24
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;
}
}

2
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';

12
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), '/') || '.';
}

12
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;
}

5
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;

20
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<Builders.Protractor, E2EOptions>;
interface WorkspaceCLISchema {
warnings?: Record<string, boolean>;
schematicCollections?: string[];
defaultCollection?: string;
}
export interface WorkspaceSchema {
version: 1;
defaultProject?: string;
cli?: WorkspaceCLISchema;
projects: {
[key: string]: WorkspaceProject<ProjectType.Application | ProjectType.Library>;
@ -165,6 +172,7 @@ export interface WorkspaceProject<TProjectType extends ProjectType = ProjectType
* Tool options.
*/
architect?: WorkspaceTargets<TProjectType>;
/**
* Tool options.
*/

12
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<string> {
@ -58,14 +58,12 @@ class TreeWorkspaceHost implements workspaces.WorkspaceHost {
export function updateWorkspace(
updater: (workspace: WorkspaceDefinition) => void | Rule | PromiseLike<void | Rule>,
): 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;
};

14
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<T = any>(
// @typescript-eslint/no-explicit-any
notFoundValue: T = NOT_FOUND_VALUE as unknown as any,
): Promise<Project | T> {
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;

Loading…
Cancel
Save