diff --git a/docs/docs/schematics/upgrade.mdx b/docs/docs/schematics/upgrade.mdx index d6808e825..e1116e557 100644 --- a/docs/docs/schematics/upgrade.mdx +++ b/docs/docs/schematics/upgrade.mdx @@ -1,13 +1,13 @@ --- title: Upgrade -description: Schematics - Upgrade | Transloco Angular i18n +description: Schematics - Upgrade [Legacy] | Transloco Angular i18n --- -## Upgrade +## Upgrade [Legacy and will be removed in v7] ### Overview -The Library's upgrade script from lower versions. For more information about the script see: +The Library's upgrade script from v1 to v2. For more information about the script see: [v2-upgrade.md](https://github.com/ngneat/transloco/tree/master/libs/transloco-schematics/src/upgrade/v2-upgrade.md) ### Command diff --git a/libs/transloco-schematics/src/collection.json b/libs/transloco-schematics/src/collection.json index a6269159b..d3d7c5cdc 100644 --- a/libs/transloco-schematics/src/collection.json +++ b/libs/transloco-schematics/src/collection.json @@ -1,57 +1,57 @@ { "extends": ["@schematics/angular"], - "$schema": "../../../../@angular-devkit/schematics/collection-schema.json", + "$schema": "../../../node_modules/@angular-devkit/schematics/collection-schema.json", "schematics": { "ng-add": { "aliases": ["init"], - "factory": "./ng-add/", + "factory": "./ng-add/index", "schema": "./ng-add/schema.json", "description": "Add transloco configuration for root module." }, "component": { "aliases": ["c"], - "factory": "./component/", + "factory": "./component/index", "schema": "../../../../@schematics/angular/component/schema.json", "description": "Add component with translation." }, "migrate": { "aliases": ["m"], - "factory": "./migrate/", + "factory": "./migrate/index", "schema": "./migrate/schema.json", "description": "Run a migration script from @ngx-translate." }, "scope": { "aliases": ["s"], - "factory": "./scope/", + "factory": "./scope/index", "schema": "./scope/schema.json", "description": "Add transloco scope." }, "upgrade": { "aliases": ["up"], - "factory": "./upgrade/", + "factory": "./upgrade/index", "schema": "./upgrade/schema.json", "description": "Run an upgrade script." }, "keys-manager": { "aliases": ["km"], - "factory": "./keys-manager/", + "factory": "./keys-manager/index", "schema": "./keys-manager/schema.json", "description": "Add Transloco Keys Manager plugin to your project." }, "join": { "aliases": ["build"], - "factory": "./join/", + "factory": "./join/index", "schema": "./join/schema.json", "description": "Join the translation files into one." }, "split": { "aliases": ["sp"], - "factory": "./split/", + "factory": "./split/index", "schema": "./split/schema.json", "description": "split the translated files over the project's translation files." }, "ng-migrate": { - "factory": "./ng-migrate/", + "factory": "./ng-migrate/index", "schema": "./ng-migrate/schema.json", "description": "split the translated files over the project's translation files." } diff --git a/libs/transloco-schematics/src/join/schema.json b/libs/transloco-schematics/src/join/schema.json index 42d22ef8b..94747b387 100644 --- a/libs/transloco-schematics/src/join/schema.json +++ b/libs/transloco-schematics/src/join/schema.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/schema", - "$id": "SchematicsTranslocoJoin", + "$id": "transloco-join", "title": "Merge all of the project's translation files into one.", "type": "object", "properties": { diff --git a/libs/transloco-schematics/src/keys-manager/schema.json b/libs/transloco-schematics/src/keys-manager/schema.json index f32b82d83..fa05a57b0 100644 --- a/libs/transloco-schematics/src/keys-manager/schema.json +++ b/libs/transloco-schematics/src/keys-manager/schema.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/schema", - "$id": "SchematicsTranslocoKeysManager", - "title": "Add Transloco Keys Manager to your project", + "$id": "transloco-keys-manager", + "title": "Adds Transloco Keys Manager to a project", "type": "object", "properties": { "strategy": { diff --git a/libs/transloco-schematics/src/migrate/schema.json b/libs/transloco-schematics/src/migrate/schema.json index aebe72f0c..86182040d 100644 --- a/libs/transloco-schematics/src/migrate/schema.json +++ b/libs/transloco-schematics/src/migrate/schema.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/schema", - "$id": "SchematicsTranslocoMigrate", + "$id": "transloco-migrate", "title": "Run a migration script from @ngx-translate.", "type": "object", "properties": { diff --git a/libs/transloco-schematics/src/ng-add/index.ts b/libs/transloco-schematics/src/ng-add/index.ts index 1d266767e..18b61b1b6 100644 --- a/libs/transloco-schematics/src/ng-add/index.ts +++ b/libs/transloco-schematics/src/ng-add/index.ts @@ -1,4 +1,4 @@ -import * as _path from 'node:path'; +import { dirname } from 'node:path'; import { chain, @@ -7,24 +7,24 @@ import { SchematicContext, Tree, } from '@angular-devkit/schematics'; - -import { stringifyList } from '../utils/array'; -import { getProject, setEnvironments } from '../utils/projects'; -import { checkIfTranslationFilesExist } from '../utils/translations'; -import { createConfig } from '../utils/transloco'; import { getAppModulePath, isStandaloneApp, -} from '../utils/ng-schematics-utils/ng-ast-utils'; +} from '@schematics/angular/utility/ng-ast-utils'; import { findBootstrapApplicationCall, getMainFilePath, -} from '../utils/ng-schematics-utils/standalone/util'; +} from '@schematics/angular/utility/standalone/util'; import { addRootImport, addRootProvider, -} from '../utils/ng-schematics-utils/standalone'; -import { findAppConfig } from '../utils/ng-schematics-utils/standalone/app_config'; +} from '@schematics/angular/utility/standalone/rules'; +import { findAppConfig } from '@schematics/angular/utility/standalone/app_config'; + +import { stringifyList } from '../utils/array'; +import { getProject, setEnvironments } from '../utils/projects'; +import { checkIfTranslationFilesExist } from '../utils/translations'; +import { createConfig } from '../utils/transloco'; import { Loaders, SchemaOptions } from './schema'; import { createLoaderFile } from './generators/http-loader.gen'; @@ -55,7 +55,7 @@ function resolveLoaderPath({ host, mainPath, isStandalone, modulePath }) { const bootstrapCall = findBootstrapApplicationCall(host, mainPath); resolved = findAppConfig(bootstrapCall, host, mainPath)?.filePath || mainPath; - resolved = _path.dirname(resolved); + resolved = dirname(resolved); } return resolved; @@ -75,7 +75,7 @@ export default function (options: SchemaOptions): Rule { const isStandalone = isStandaloneApp(host, mainPath); const modulePath = isStandalone ? '' - : _path.dirname(getAppModulePath(host, mainPath)); + : dirname(getAppModulePath(host, mainPath)); const actions = []; diff --git a/libs/transloco-schematics/src/ng-add/schema.json b/libs/transloco-schematics/src/ng-add/schema.json index a5f5ead4f..d56ebec17 100644 --- a/libs/transloco-schematics/src/ng-add/schema.json +++ b/libs/transloco-schematics/src/ng-add/schema.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/schema", - "$id": "SchematicsTranslocoAdd", - "title": "Add Transloco to Your Project", + "$id": "transloco-ng-add", + "title": "Adds Transloco to a project", "type": "object", "properties": { "langs": { @@ -11,6 +11,12 @@ "default": "en, es", "alias": "la" }, + "ssr": { + "description": "If the user is working with server side rendering.", + "x-prompt": "\uD83D\uDE80 Are you working with server side rendering?", + "type": "boolean", + "default": false + }, "loader": { "description": "The loader strategy for the translate files.", "type": "string", @@ -25,12 +31,6 @@ "enum": ["JSON", "Typescript"], "alias": "t" }, - "ssr": { - "description": "If the user is working with server side rendering.", - "x-prompt": "\uD83D\uDE80 Are you working with server side rendering?", - "type": "boolean", - "default": false - }, "path": { "type": "string", "default": "assets/i18n/", diff --git a/libs/transloco-schematics/src/ng-add/schema.ts b/libs/transloco-schematics/src/ng-add/schema.ts index 5d7f463b7..8197a8ec5 100644 --- a/libs/transloco-schematics/src/ng-add/schema.ts +++ b/libs/transloco-schematics/src/ng-add/schema.ts @@ -14,7 +14,7 @@ export interface SchemaOptions { */ langs: string; /** - * The languages of the project. + * The translations loader */ loader: Loaders; /** diff --git a/libs/transloco-schematics/src/ng-migrate/schema.json b/libs/transloco-schematics/src/ng-migrate/schema.json index 4304474da..ffcff8584 100644 --- a/libs/transloco-schematics/src/ng-migrate/schema.json +++ b/libs/transloco-schematics/src/ng-migrate/schema.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/schema", - "$id": "SchematicsTranslocoNgMigrate", - "title": "Migration Script from Angular", + "$id": "transloco-ng-migrate", + "title": "Run a migration script from Angular i18n", "type": "object", "properties": { "langs": { diff --git a/libs/transloco-schematics/src/scope/index.ts b/libs/transloco-schematics/src/scope/index.ts index 0e24a8da3..afaba2121 100644 --- a/libs/transloco-schematics/src/scope/index.ts +++ b/libs/transloco-schematics/src/scope/index.ts @@ -11,20 +11,20 @@ import { chain, } from '@angular-devkit/schematics'; import { ScriptTarget, createSourceFile } from 'typescript'; - -import { LIB_NAME } from '../schematics.consts'; -import { coerceArray, stringifyList } from '../utils/array'; import { addProviderToModule, insertImport, addImportToModule, -} from '../utils/ng-schematics-utils/ast-utils'; +} from '@schematics/angular/utility/ast-utils'; +import { applyChangesToFile } from '@schematics/angular/utility/standalone/util'; +import { Change } from '@schematics/angular/utility/change'; + +import { LIB_NAME } from '../schematics.consts'; +import { coerceArray, stringifyList } from '../utils/array'; import { findModuleFromOptions } from '../utils/find-module'; import { getProject, getProjectPath } from '../utils/projects'; import { createTranslateFilesFromOptions } from '../utils/translations'; import { getConfig } from '../utils/config'; -import { applyChangesToFile } from '../utils/ng-schematics-utils/standalone/util'; -import { Change } from '../utils/ng-schematics-utils/change'; import { SchemaOptions } from './schema'; diff --git a/libs/transloco-schematics/src/scope/schema.json b/libs/transloco-schematics/src/scope/schema.json index d8879f5f9..869ae72af 100644 --- a/libs/transloco-schematics/src/scope/schema.json +++ b/libs/transloco-schematics/src/scope/schema.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/schema", - "$id": "SchematicsTranslocoScope", + "$id": "transloco-scope", "title": "Add new transloco scope module, along with translation files.", "type": "object", "properties": { diff --git a/libs/transloco-schematics/src/split/schema.json b/libs/transloco-schematics/src/split/schema.json index 716486013..d8dafe402 100644 --- a/libs/transloco-schematics/src/split/schema.json +++ b/libs/transloco-schematics/src/split/schema.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/schema", - "$id": "SchematicsTranslocoSplit", + "$id": "transloco-split", "title": "Split translated files between the project's translation files.", "type": "object", "properties": { diff --git a/libs/transloco-schematics/src/upgrade/schema.json b/libs/transloco-schematics/src/upgrade/schema.json index a81887e42..bdae76597 100644 --- a/libs/transloco-schematics/src/upgrade/schema.json +++ b/libs/transloco-schematics/src/upgrade/schema.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/schema", - "$id": "SchematicsTranslocoUpgrade", - "title": "Run an upgrade script.", + "$id": "transloco-upgrade", + "title": "Run an upgrade script [Legacy and will be removed in v7]", "type": "object", "properties": { "path": { diff --git a/libs/transloco-schematics/src/utils/ng-schematics-utils/add-declaration-to-ng-module.ts b/libs/transloco-schematics/src/utils/ng-schematics-utils/add-declaration-to-ng-module.ts deleted file mode 100644 index 807d9fcb2..000000000 --- a/libs/transloco-schematics/src/utils/ng-schematics-utils/add-declaration-to-ng-module.ts +++ /dev/null @@ -1,85 +0,0 @@ -/** - * @license - * 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 - */ - -import { Rule, Tree, strings } from '@angular-devkit/schematics'; -import * as ts from 'typescript'; - -import { - addDeclarationToModule, - addSymbolToNgModuleMetadata, -} from './ast-utils'; -import { InsertChange } from './change'; -import { buildRelativePath } from './find-module'; - -export interface DeclarationToNgModuleOptions { - module?: string; - path?: string; - name: string; - flat?: boolean; - export?: boolean; - type: string; - skipImport?: boolean; - standalone?: boolean; -} - -export function addDeclarationToNgModule( - options: DeclarationToNgModuleOptions -): Rule { - return (host: Tree) => { - const modulePath = options.module; - if (options.skipImport || options.standalone || !modulePath) { - return host; - } - - const sourceText = host.readText(modulePath); - const source = ts.createSourceFile( - modulePath, - sourceText, - ts.ScriptTarget.Latest, - true - ); - - const filePath = - `/${options.path}/` + - (options.flat ? '' : strings.dasherize(options.name) + '/') + - strings.dasherize(options.name) + - (options.type ? '.' : '') + - strings.dasherize(options.type); - - const importPath = buildRelativePath(modulePath, filePath); - const classifiedName = - strings.classify(options.name) + strings.classify(options.type); - const changes = addDeclarationToModule( - source, - modulePath, - classifiedName, - importPath - ); - - if (options.export) { - changes.push( - ...addSymbolToNgModuleMetadata( - source, - modulePath, - 'exports', - classifiedName - ) - ); - } - - const recorder = host.beginUpdate(modulePath); - for (const change of changes) { - if (change instanceof InsertChange) { - recorder.insertLeft(change.pos, change.toAdd); - } - } - host.commitUpdate(recorder); - - return host; - }; -} diff --git a/libs/transloco-schematics/src/utils/ng-schematics-utils/ast-utils.ts b/libs/transloco-schematics/src/utils/ng-schematics-utils/ast-utils.ts deleted file mode 100644 index 25ebd6804..000000000 --- a/libs/transloco-schematics/src/utils/ng-schematics-utils/ast-utils.ts +++ /dev/null @@ -1,809 +0,0 @@ -/** - * @license - * 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 - */ - -import { tags } from '@angular-devkit/core'; -import * as ts from 'typescript'; - -import { Change, InsertChange, NoopChange } from './change'; - -/** - * 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 alias Alias that the symbol should be inserted under. - * @return Change - */ -export function insertImport( - source: ts.SourceFile, - fileToEdit: string, - symbolName: string, - fileName: string, - isDefault = false, - alias?: string -): Change { - const rootNode = source; - 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) => { - return ( - ts.isStringLiteralLike(node.moduleSpecifier) && - node.moduleSpecifier.text === fileName - ); - }); - - if (relevantImports.length > 0) { - const hasNamespaceImport = relevantImports.some((node) => { - return ( - node.importClause?.namedBindings?.kind === ts.SyntaxKind.NamespaceImport - ); - }); - - // if imports * from fileName, don't add symbolName - if (hasNamespaceImport) { - return new NoopChange(); - } - - 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 ( - !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, - `, ${importExpression}`, - fileToEdit, - fallbackPos - ); - } - - return new NoopChange(); - } - - // no such import declaration exists - const useStrict = findNodes(rootNode, ts.isStringLiteral).filter( - (n) => n.text === 'use strict' - ); - let fallbackPos = 0; - if (useStrict.length > 0) { - fallbackPos = useStrict[0].end; - } - const open = isDefault ? '' : '{ '; - const close = isDefault ? '' : ' }'; - // 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 toInsert = - `${separator}import ${open}${importExpression}${close}` + - ` from '${fileName}'${insertAtBeginning ? ';\n' : ''}`; - - return insertAfterLastOccurrence( - allImports, - toInsert, - fileToEdit, - fallbackPos, - ts.SyntaxKind.StringLiteral - ); -} - -/** - * Find all nodes from the AST in the subtree of node of SyntaxKind kind. - * @param node - * @param kind - * @param max The maximum number of items to return. - * @param recursive Continue looking for nodes of kind recursive until end - * the last child even when node of kind has been found. - * @return all nodes of kind, or [] if none is found - */ -export function findNodes( - node: ts.Node, - kind: ts.SyntaxKind, - max?: number, - recursive?: boolean -): ts.Node[]; - -/** - * Find all nodes from the AST in the subtree that satisfy a type guard. - * @param node - * @param guard - * @param max The maximum number of items to return. - * @param recursive Continue looking for nodes of kind recursive until end - * the last child even when node of kind has been found. - * @return all nodes that satisfy the type guard, or [] if none is found - */ -export function findNodes( - node: ts.Node, - guard: (node: ts.Node) => node is T, - max?: number, - recursive?: boolean -): T[]; - -export function findNodes( - node: ts.Node, - kindOrGuard: ts.SyntaxKind | ((node: ts.Node) => node is T), - max = Infinity, - recursive = false -): T[] { - if (!node || max == 0) { - return []; - } - - const test = - typeof kindOrGuard === 'function' - ? kindOrGuard - : (node: ts.Node): node is T => node.kind === kindOrGuard; - - const arr: T[] = []; - if (test(node)) { - arr.push(node); - max--; - } - if (max > 0 && (recursive || !test(node))) { - for (const child of node.getChildren()) { - findNodes(child, test, max, recursive).forEach((node) => { - if (max > 0) { - arr.push(node); - } - max--; - }); - - if (max <= 0) { - break; - } - } - } - - return arr; -} - -/** - * Get all the nodes from a source. - * @param sourceFile The source file object. - * @returns {Array} An array of all the nodes in the source. - */ -export function getSourceNodes(sourceFile: ts.SourceFile): ts.Node[] { - const nodes: ts.Node[] = [sourceFile]; - const result: ts.Node[] = []; - - while (nodes.length > 0) { - const node = nodes.shift(); - - if (node) { - result.push(node); - if (node.getChildCount(sourceFile) >= 0) { - nodes.unshift(...node.getChildren()); - } - } - } - - return result; -} - -export function findNode( - node: ts.Node, - kind: ts.SyntaxKind, - text: string -): ts.Node | null { - if (node.kind === kind && node.getText() === text) { - return node; - } - - let foundNode: ts.Node | null = null; - ts.forEachChild(node, (childNode) => { - foundNode = foundNode || findNode(childNode, kind, text); - }); - - return foundNode; -} - -/** - * Helper for sorting nodes. - * @return function to sort nodes in increasing order of position in sourceFile - */ -function nodesByPosition(first: ts.Node, second: ts.Node): number { - return first.getStart() - second.getStart(); -} - -/** - * Insert `toInsert` after the last occurence of `ts.SyntaxKind[nodes[i].kind]` - * or after the last of occurence of `syntaxKind` if the last occurence is a sub child - * of ts.SyntaxKind[nodes[i].kind] and save the changes in file. - * - * @param nodes insert after the last occurence of nodes - * @param toInsert string to insert - * @param file file to insert changes into - * @param fallbackPos position to insert if toInsert happens to be the first occurence - * @param syntaxKind the ts.SyntaxKind of the subchildren to insert after - * @return Change instance - * @throw Error if toInsert is first occurence but fall back is not set - */ -export function insertAfterLastOccurrence( - nodes: ts.Node[] | ts.NodeArray, - toInsert: string, - file: string, - fallbackPos: number, - syntaxKind?: ts.SyntaxKind -): Change { - let lastItem: ts.Node | undefined; - for (const node of nodes) { - if (!lastItem || lastItem.getStart() < node.getStart()) { - lastItem = node; - } - } - if (syntaxKind && lastItem) { - lastItem = findNodes(lastItem, syntaxKind).sort(nodesByPosition).pop(); - } - if (!lastItem && fallbackPos == undefined) { - throw new Error( - `tried to insert ${toInsert} as first occurence with no fallback position` - ); - } - const lastItemPosition: number = lastItem ? lastItem.getEnd() : fallbackPos; - - return new InsertChange(file, lastItemPosition, toInsert); -} - -function _angularImportsFromNode(node: ts.ImportDeclaration): { - [name: string]: string; -} { - const ms = node.moduleSpecifier; - let modulePath: string; - switch (ms.kind) { - case ts.SyntaxKind.StringLiteral: - modulePath = (ms as ts.StringLiteral).text; - break; - default: - return {}; - } - - if (!modulePath.startsWith('@angular/')) { - return {}; - } - - if (node.importClause) { - if (node.importClause.name) { - // This is of the form `import Name from 'path'`. Ignore. - return {}; - } else if (node.importClause.namedBindings) { - const nb = node.importClause.namedBindings; - if (nb.kind == ts.SyntaxKind.NamespaceImport) { - // This is of the form `import * as name from 'path'`. Return `name.`. - return { - [nb.name.text + '.']: modulePath, - }; - } else { - // This is of the form `import {a,b,c} from 'path'` - const namedImports = nb; - - return namedImports.elements - .map((is: ts.ImportSpecifier) => - is.propertyName ? is.propertyName.text : is.name.text - ) - .reduce((acc: { [name: string]: string }, curr: string) => { - acc[curr] = modulePath; - - return acc; - }, {}); - } - } - - return {}; - } else { - // This is of the form `import 'path';`. Nothing to do. - return {}; - } -} - -export function getDecoratorMetadata( - source: ts.SourceFile, - identifier: string, - module: string -): ts.Node[] { - const angularImports = findNodes(source, ts.isImportDeclaration) - .map((node) => _angularImportsFromNode(node)) - .reduce((acc, current) => { - for (const key of Object.keys(current)) { - acc[key] = current[key]; - } - - return acc; - }, {}); - - return getSourceNodes(source) - .filter((node) => { - return ( - node.kind == ts.SyntaxKind.Decorator && - (node as ts.Decorator).expression.kind == ts.SyntaxKind.CallExpression - ); - }) - .map((node) => (node as ts.Decorator).expression as ts.CallExpression) - .filter((expr) => { - if (expr.expression.kind == ts.SyntaxKind.Identifier) { - const id = expr.expression as ts.Identifier; - - return id.text == identifier && angularImports[id.text] === module; - } else if ( - expr.expression.kind == ts.SyntaxKind.PropertyAccessExpression - ) { - // This covers foo.NgModule when importing * as foo. - const paExpr = expr.expression as ts.PropertyAccessExpression; - // If the left expression is not an identifier, just give up at that point. - if (paExpr.expression.kind !== ts.SyntaxKind.Identifier) { - return false; - } - - const id = paExpr.name.text; - const moduleId = (paExpr.expression as ts.Identifier).text; - - return id === identifier && angularImports[moduleId + '.'] === module; - } - - return false; - }) - .filter( - (expr) => - expr.arguments[0] && - expr.arguments[0].kind == ts.SyntaxKind.ObjectLiteralExpression - ) - .map((expr) => expr.arguments[0] as ts.ObjectLiteralExpression); -} - -export function getMetadataField( - node: ts.ObjectLiteralExpression, - metadataField: string -): ts.ObjectLiteralElement[] { - return ( - node.properties - .filter(ts.isPropertyAssignment) - // Filter out every fields that's not "metadataField". Also handles string literals - // (but not expressions). - .filter(({ name }) => { - return ( - (ts.isIdentifier(name) || ts.isStringLiteral(name)) && - name.text === metadataField - ); - }) - ); -} - -export function addSymbolToNgModuleMetadata( - source: ts.SourceFile, - ngModulePath: string, - metadataField: string, - symbolName: string, - importPath: string | null = null -): Change[] { - const nodes = getDecoratorMetadata(source, 'NgModule', '@angular/core'); - const node = nodes[0]; - - // Find the decorator declaration. - if (!node || !ts.isObjectLiteralExpression(node)) { - return []; - } - - // Get all the children property assignment of object literals. - const matchingProperties = getMetadataField(node, metadataField); - - if (matchingProperties.length == 0) { - // We haven't found the field in the metadata declaration. Insert a new field. - let position: number; - let toInsert: string; - if (node.properties.length == 0) { - position = node.getEnd() - 1; - toInsert = `\n ${metadataField}: [\n${tags.indentBy( - 4 - )`${symbolName}`}\n ]\n`; - } else { - const childNode = node.properties[node.properties.length - 1]; - position = childNode.getEnd(); - // Get the indentation of the last element, if any. - const text = childNode.getFullText(source); - const matches = text.match(/^(\r?\n)(\s*)/); - if (matches) { - toInsert = - `,${matches[0]}${metadataField}: [${matches[1]}` + - `${tags.indentBy(matches[2].length + 2)`${symbolName}`}${ - matches[0] - }]`; - } else { - toInsert = `, ${metadataField}: [${symbolName}]`; - } - } - if (importPath !== null) { - return [ - new InsertChange(ngModulePath, position, toInsert), - insertImport( - source, - ngModulePath, - symbolName.replace(/\..*$/, ''), - importPath - ), - ]; - } else { - return [new InsertChange(ngModulePath, position, toInsert)]; - } - } - const assignment = matchingProperties[0]; - - // If it's not an array, nothing we can do really. - if ( - !ts.isPropertyAssignment(assignment) || - !ts.isArrayLiteralExpression(assignment.initializer) - ) { - return []; - } - - let expresssion: ts.Expression | ts.ArrayLiteralExpression; - const assignmentInit = assignment.initializer; - const elements = assignmentInit.elements; - - if (elements.length) { - const symbolsArray = elements.map( - (node) => tags.oneLine`${node.getText()}` - ); - if (symbolsArray.includes(tags.oneLine`${symbolName}`)) { - return []; - } - - expresssion = elements[elements.length - 1]; - } else { - expresssion = assignmentInit; - } - - let toInsert: string; - let position = expresssion.getEnd(); - if (ts.isArrayLiteralExpression(expresssion)) { - // 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 matches = text.match(/^(\r?\n)(\s*)/); - if (matches) { - toInsert = `,${matches[1]}${tags.indentBy( - matches[2].length - )`${symbolName}`}`; - } else { - toInsert = `, ${symbolName}`; - } - } - - if (importPath !== null) { - return [ - new InsertChange(ngModulePath, position, toInsert), - insertImport( - source, - ngModulePath, - symbolName.replace(/\..*$/, ''), - importPath - ), - ]; - } - - return [new InsertChange(ngModulePath, position, toInsert)]; -} - -/** - * Custom function to insert a declaration (component, pipe, directive) - * into NgModule declarations. It also imports the component. - */ -export function addDeclarationToModule( - source: ts.SourceFile, - modulePath: string, - classifiedName: string, - importPath: string -): Change[] { - return addSymbolToNgModuleMetadata( - source, - modulePath, - 'declarations', - classifiedName, - importPath - ); -} - -/** - * Custom function to insert an NgModule into NgModule imports. It also imports the module. - */ -export function addImportToModule( - source: ts.SourceFile, - modulePath: string, - classifiedName: string, - importPath: string -): Change[] { - return addSymbolToNgModuleMetadata( - source, - modulePath, - 'imports', - classifiedName, - importPath - ); -} - -/** - * Custom function to insert a provider into NgModule. It also imports it. - */ -export function addProviderToModule( - source: ts.SourceFile, - modulePath: string, - classifiedName: string, - importPath: string -): Change[] { - return addSymbolToNgModuleMetadata( - source, - modulePath, - 'providers', - classifiedName, - importPath - ); -} - -/** - * Custom function to insert an export into NgModule. It also imports it. - */ -export function addExportToModule( - source: ts.SourceFile, - modulePath: string, - classifiedName: string, - importPath: string -): Change[] { - return addSymbolToNgModuleMetadata( - source, - modulePath, - 'exports', - classifiedName, - importPath - ); -} - -/** - * Custom function to insert an export into NgModule. It also imports it. - */ -export function addBootstrapToModule( - source: ts.SourceFile, - modulePath: string, - classifiedName: string, - importPath: string -): Change[] { - return addSymbolToNgModuleMetadata( - source, - modulePath, - 'bootstrap', - classifiedName, - importPath - ); -} - -/** - * Determine if an import already exists. - */ -export function isImported( - source: ts.SourceFile, - classifiedName: string, - importPath: string -): boolean { - const allNodes = getSourceNodes(source); - const matchingNodes = allNodes - .filter(ts.isImportDeclaration) - .filter( - (imp) => - ts.isStringLiteral(imp.moduleSpecifier) && - imp.moduleSpecifier.text === importPath - ) - .filter((imp) => { - if (!imp.importClause) { - return false; - } - const nodes = findNodes(imp.importClause, ts.isImportSpecifier).filter( - (n) => n.getText() === classifiedName - ); - - return nodes.length > 0; - }); - - return matchingNodes.length > 0; -} - -/** - * Returns the RouterModule declaration from NgModule metadata, if any. - */ -export function getRouterModuleDeclaration( - source: ts.SourceFile -): ts.Expression | undefined { - const result = getDecoratorMetadata(source, 'NgModule', '@angular/core'); - const node = result[0]; - if (!node || !ts.isObjectLiteralExpression(node)) { - return undefined; - } - - const matchingProperties = getMetadataField(node, 'imports'); - if (!matchingProperties) { - return; - } - - const assignment = matchingProperties[0] as ts.PropertyAssignment; - - if (assignment.initializer.kind !== ts.SyntaxKind.ArrayLiteralExpression) { - return; - } - - const arrLiteral = assignment.initializer as ts.ArrayLiteralExpression; - - return arrLiteral.elements - .filter((el) => el.kind === ts.SyntaxKind.CallExpression) - .find((el) => (el as ts.Identifier).getText().startsWith('RouterModule')); -} - -/** - * Adds a new route declaration to a router module (i.e. has a RouterModule declaration) - */ -export function addRouteDeclarationToModule( - source: ts.SourceFile, - fileToAdd: string, - routeLiteral: string -): Change { - const routerModuleExpr = getRouterModuleDeclaration(source); - if (!routerModuleExpr) { - throw new Error( - `Couldn't find a route declaration in ${fileToAdd}.\n` + - `Use the '--module' option to specify a different routing module.` - ); - } - const scopeConfigMethodArgs = (routerModuleExpr as ts.CallExpression) - .arguments; - if (!scopeConfigMethodArgs.length) { - const { line } = source.getLineAndCharacterOfPosition( - routerModuleExpr.getStart() - ); - throw new Error( - `The router module method doesn't have arguments ` + - `at line ${line} in ${fileToAdd}` - ); - } - - let routesArr: ts.ArrayLiteralExpression | undefined; - const routesArg = scopeConfigMethodArgs[0]; - - // Check if the route declarations array is - // an inlined argument of RouterModule or a standalone variable - if (ts.isArrayLiteralExpression(routesArg)) { - routesArr = routesArg; - } else { - const routesVarName = routesArg.getText(); - let routesVar; - if (routesArg.kind === ts.SyntaxKind.Identifier) { - routesVar = source.statements.filter(ts.isVariableStatement).find((v) => { - return ( - v.declarationList.declarations[0].name.getText() === routesVarName - ); - }); - } - - if (!routesVar) { - const { line } = source.getLineAndCharacterOfPosition( - routesArg.getStart() - ); - throw new Error( - `No route declaration array was found that corresponds ` + - `to router module at line ${line} in ${fileToAdd}` - ); - } - - routesArr = findNodes( - routesVar, - ts.SyntaxKind.ArrayLiteralExpression, - 1 - )[0] as ts.ArrayLiteralExpression; - } - - const occurrencesCount = routesArr.elements.length; - const text = routesArr.getFullText(source); - - let route: string = routeLiteral; - let insertPos = routesArr.elements.pos; - - if (occurrencesCount > 0) { - const lastRouteLiteral = [...routesArr.elements].pop() as ts.Expression; - const lastRouteIsWildcard = - ts.isObjectLiteralExpression(lastRouteLiteral) && - lastRouteLiteral.properties.some( - (n) => - ts.isPropertyAssignment(n) && - ts.isIdentifier(n.name) && - n.name.text === 'path' && - ts.isStringLiteral(n.initializer) && - n.initializer.text === '**' - ); - - const indentation = text.match(/\r?\n(\r?)\s*/) || []; - const routeText = `${indentation[0] || ' '}${routeLiteral}`; - - // Add the new route before the wildcard route - // otherwise we'll always redirect to the wildcard route - if (lastRouteIsWildcard) { - insertPos = lastRouteLiteral.pos; - route = `${routeText},`; - } else { - insertPos = lastRouteLiteral.end; - route = `,${routeText}`; - } - } - - 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/libs/transloco-schematics/src/utils/ng-schematics-utils/change.ts b/libs/transloco-schematics/src/utils/ng-schematics-utils/change.ts deleted file mode 100644 index 6aa868a8d..000000000 --- a/libs/transloco-schematics/src/utils/ng-schematics-utils/change.ts +++ /dev/null @@ -1,157 +0,0 @@ -/** - * @license - * 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 - */ - -import { UpdateRecorder } from '@angular-devkit/schematics'; - -export interface Host { - write(path: string, content: string): Promise; - read(path: string): Promise; -} - -export interface Change { - apply(host: Host): Promise; - - // The file this change should be applied to. Some changes might not apply to - // a file (maybe the config). - readonly path: string | null; - - // The order this change should be applied. Normally the position inside the file. - // Changes are applied from the bottom of a file to the top. - readonly order: number; - - // The description of this change. This will be outputted in a dry or verbose run. - readonly description: string; -} - -/** - * An operation that does nothing. - */ -export class NoopChange implements Change { - description = 'No operation.'; - order = Infinity; - path = null; - apply() { - return Promise.resolve(); - } -} - -/** - * Will add text to the source code. - */ -export class InsertChange implements Change { - order: number; - description: string; - - constructor(public path: string, public pos: number, public toAdd: string) { - if (pos < 0) { - throw new Error('Negative positions are invalid'); - } - this.description = `Inserted ${toAdd} into position ${pos} of ${path}`; - this.order = pos; - } - - /** - * This method does not insert spaces if there is none in the original string. - */ - apply(host: Host) { - return host.read(this.path).then((content) => { - const prefix = content.substring(0, this.pos); - const suffix = content.substring(this.pos); - - return host.write(this.path, `${prefix}${this.toAdd}${suffix}`); - }); - } -} - -/** - * Will remove text from the source code. - */ -export class RemoveChange implements Change { - order: number; - description: string; - - constructor( - public path: string, - private pos: number, - public toRemove: string - ) { - if (pos < 0) { - throw new Error('Negative positions are invalid'); - } - this.description = `Removed ${toRemove} into position ${pos} of ${path}`; - this.order = pos; - } - - apply(host: Host): Promise { - return host.read(this.path).then((content) => { - const prefix = content.substring(0, this.pos); - const suffix = content.substring(this.pos + this.toRemove.length); - - // TODO: throw error if toRemove doesn't match removed string. - return host.write(this.path, `${prefix}${suffix}`); - }); - } -} - -/** - * Will replace text from the source code. - */ -export class ReplaceChange implements Change { - order: number; - description: string; - - constructor( - public path: string, - private pos: number, - public oldText: string, - public newText: string - ) { - if (pos < 0) { - throw new Error('Negative positions are invalid'); - } - this.description = `Replaced ${oldText} into position ${pos} of ${path} with ${newText}`; - this.order = pos; - } - - apply(host: Host): Promise { - 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); - - if (text !== this.oldText) { - return Promise.reject( - new Error(`Invalid replace: "${text}" != "${this.oldText}".`) - ); - } - - // TODO: throw error if oldText doesn't match removed string. - return host.write(this.path, `${prefix}${this.newText}${suffix}`); - }); - } -} - -export function applyToUpdateRecorder( - recorder: UpdateRecorder, - changes: Change[] -): void { - for (const change of changes) { - if (change instanceof InsertChange) { - recorder.insertLeft(change.pos, change.toAdd); - } else if (change instanceof RemoveChange) { - recorder.remove(change.order, change.toRemove.length); - } else if (change instanceof ReplaceChange) { - recorder.remove(change.order, change.oldText.length); - recorder.insertLeft(change.order, change.newText); - } else if (!(change instanceof NoopChange)) { - throw new Error( - 'Unknown Change type encountered when updating a recorder.' - ); - } - } -} diff --git a/libs/transloco-schematics/src/utils/ng-schematics-utils/dependencies.ts b/libs/transloco-schematics/src/utils/ng-schematics-utils/dependencies.ts deleted file mode 100644 index 9a0a1eeb1..000000000 --- a/libs/transloco-schematics/src/utils/ng-schematics-utils/dependencies.ts +++ /dev/null @@ -1,81 +0,0 @@ -/** - * @license - * 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 - */ - -import { Tree } from '@angular-devkit/schematics'; - -import { JSONFile } from './json-file'; - -const PKG_JSON_PATH = '/package.json'; -export enum NodeDependencyType { - Default = 'dependencies', - Dev = 'devDependencies', - Peer = 'peerDependencies', - Optional = 'optionalDependencies', -} - -export interface NodeDependency { - type: NodeDependencyType; - name: string; - version: string; - overwrite?: boolean; -} - -const ALL_DEPENDENCY_TYPE = [ - NodeDependencyType.Default, - NodeDependencyType.Dev, - NodeDependencyType.Optional, - NodeDependencyType.Peer, -]; - -export function addPackageJsonDependency( - tree: Tree, - dependency: NodeDependency, - pkgJsonPath = PKG_JSON_PATH -): void { - const json = new JSONFile(tree, pkgJsonPath); - - const { overwrite, type, name, version } = dependency; - const path = [type, name]; - if (overwrite || !json.get(path)) { - json.modify(path, version); - } -} - -export function removePackageJsonDependency( - tree: Tree, - name: string, - pkgJsonPath = PKG_JSON_PATH -): void { - const json = new JSONFile(tree, pkgJsonPath); - - for (const depType of ALL_DEPENDENCY_TYPE) { - json.remove([depType, name]); - } -} - -export function getPackageJsonDependency( - tree: Tree, - name: string, - pkgJsonPath = PKG_JSON_PATH -): NodeDependency | null { - const json = new JSONFile(tree, pkgJsonPath); - - for (const depType of ALL_DEPENDENCY_TYPE) { - const version = json.get([depType, name]); - - if (typeof version === 'string') { - return { - type: depType, - name: name, - version, - }; - } - } - - return null; -} diff --git a/libs/transloco-schematics/src/utils/ng-schematics-utils/dependency.ts b/libs/transloco-schematics/src/utils/ng-schematics-utils/dependency.ts deleted file mode 100644 index b02636023..000000000 --- a/libs/transloco-schematics/src/utils/ng-schematics-utils/dependency.ts +++ /dev/null @@ -1,176 +0,0 @@ -/** - * @license - * 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 - */ - -import * as path from 'node:path'; - -import { Rule, SchematicContext } from '@angular-devkit/schematics'; -import { NodePackageInstallTask } from '@angular-devkit/schematics/tasks'; - -const installTasks = new WeakMap>(); - -interface MinimalPackageManifest { - dependencies?: Record; - devDependencies?: Record; - peerDependencies?: Record; -} - -/** - * An enum used to specify the type of a dependency found within a package manifest - * file (`package.json`). - */ -export enum DependencyType { - Default = 'dependencies', - Dev = 'devDependencies', - Peer = 'peerDependencies', -} - -/** - * An enum used to specify the dependency installation behavior for the {@link addDependency} - * schematics rule. The installation behavior affects if and when {@link NodePackageInstallTask} - * will be scheduled when using the rule. - */ -export enum InstallBehavior { - /** - * No installation will occur as a result of the rule when specified. - * - * NOTE: This does not prevent other rules from scheduling a {@link NodePackageInstallTask} - * 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. - */ - Always, -} - -/** - * An enum used to specify the existing dependency behavior for the {@link addDependency} - * schematics rule. The existing behavior affects whether the named dependency will be added - * to the `package.json` when the dependency is already present with a differing specifier. - */ -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 - * notify the user of the replacement. - */ - Replace, -} - -/** - * Adds a package as a dependency to a `package.json`. By default the `package.json` located - * at the schematic's root will be used. The `manifestPath` option can be used to explicitly specify - * a `package.json` in different location. The type of the dependency can also be specified instead - * of the default of the `dependencies` section by using the `type` option for either `devDependencies` - * or `peerDependencies`. - * - * When using this rule, {@link NodePackageInstallTask} does not need to be included directly by - * a schematic. A package manager install task will be automatically scheduled as needed. - * - * @param name The name of the package to add. - * @param specifier The package specifier for the package to add. Typically a SemVer range. - * @param options An optional object that can contain the `type` of the dependency - * and/or a path (`packageJsonPath`) of a manifest file (`package.json`) to modify. - * @returns A Schematics {@link Rule} - */ -export function addDependency( - name: string, - specifier: string, - options: { - /** - * The type of the dependency determines the section of the `package.json` to which the - * 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}. - */ - existing?: ExistingBehavior; - } = {} -): Rule { - const { - type = DependencyType.Default, - packageJsonPath = '/package.json', - install = InstallBehavior.Auto, - existing = ExistingBehavior.Replace, - } = options; - - return (tree, context) => { - const manifest = tree.readJson(packageJsonPath) as MinimalPackageManifest; - const dependencySection = manifest[type]; - - if (!dependencySection) { - // Section is not present. The dependency can be added to a new object literal for the section. - manifest[type] = { [name]: specifier }; - } else { - const existingSpecifier = dependencySection[name]; - - if (existingSpecifier === specifier) { - // Already present with same specifier - return; - } - - if (existingSpecifier) { - // Already present but different specifier - - if (existing === ExistingBehavior.Skip) { - return; - } - - // ExistingBehavior.Replace is the only other behavior currently - context.logger.warn( - `Package dependency "${name}" already exists with a different specifier. ` + - `"${existingSpecifier}" will be replaced with "${specifier}".` - ); - } - - // Add new dependency in alphabetical order - const entries = Object.entries(dependencySection); - entries.push([name, specifier]); - entries.sort((a, b) => a[0].localeCompare(b[0])); - manifest[type] = Object.fromEntries(entries); - } - - tree.overwrite(packageJsonPath, JSON.stringify(manifest, null, 2)); - - const installPaths = installTasks.get(context) ?? new Set(); - if ( - install === InstallBehavior.Always || - (install === InstallBehavior.Auto && !installPaths.has(packageJsonPath)) - ) { - context.addTask( - new NodePackageInstallTask({ - workingDirectory: path.dirname(packageJsonPath), - }) - ); - installPaths.add(packageJsonPath); - installTasks.set(context, installPaths); - } - }; -} diff --git a/libs/transloco-schematics/src/utils/ng-schematics-utils/find-module.ts b/libs/transloco-schematics/src/utils/ng-schematics-utils/find-module.ts deleted file mode 100644 index 3b588ce6e..000000000 --- a/libs/transloco-schematics/src/utils/ng-schematics-utils/find-module.ts +++ /dev/null @@ -1,167 +0,0 @@ -/** - * @license - * 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 - */ - -import { - NormalizedRoot, - Path, - dirname, - join, - normalize, - relative, -} from '@angular-devkit/core'; -import { DirEntry, Tree } from '@angular-devkit/schematics'; - -export interface ModuleOptions { - module?: string; - name: string; - flat?: boolean; - path?: string; - skipImport?: boolean; - moduleExt?: string; - routingModuleExt?: string; - standalone?: boolean; -} - -export const MODULE_EXT = '.module.ts'; -export const ROUTING_MODULE_EXT = '-routing.module.ts'; - -/** - * Find the module referred by a set of options passed to the schematics. - */ -export function findModuleFromOptions( - host: Tree, - options: ModuleOptions -): Path | undefined { - if (options.standalone || options.skipImport) { - return undefined; - } - - const moduleExt = options.moduleExt || MODULE_EXT; - const routingModuleExt = options.routingModuleExt || ROUTING_MODULE_EXT; - - if (!options.module) { - const pathToCheck = (options.path || '') + '/' + options.name; - - return normalize( - findModule(host, pathToCheck, moduleExt, routingModuleExt) - ); - } else { - const modulePath = normalize(`/${options.path}/${options.module}`); - const componentPath = normalize(`/${options.path}/${options.name}`); - const moduleBaseName = normalize(modulePath).split('/').pop(); - - const candidateSet = new Set([normalize(options.path || '/')]); - - for (let dir = modulePath; dir != NormalizedRoot; dir = dirname(dir)) { - candidateSet.add(dir); - } - for (let dir = componentPath; dir != NormalizedRoot; 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) => join(c, x)); - - for (const sc of candidateFiles) { - if (host.exists(sc)) { - return normalize(sc); - } - } - } - - throw new Error( - `Specified module '${options.module}' does not exist.\n` + - `Looked in the following directories:\n ${candidatesDirs.join( - '\n ' - )}` - ); - } -} - -/** - * Function to find the "closest" module to a generated file's path. - */ -export function findModule( - host: Tree, - generateDir: string, - moduleExt = MODULE_EXT, - routingModuleExt = ROUTING_MODULE_EXT -): Path { - let dir: DirEntry | null = host.getDir('/' + generateDir); - let foundRoutingModule = false; - - while (dir) { - const allMatches = dir.subfiles.filter((p) => p.endsWith(moduleExt)); - const filteredMatches = allMatches.filter( - (p) => !p.endsWith(routingModuleExt) - ); - - foundRoutingModule = - foundRoutingModule || allMatches.length !== filteredMatches.length; - - if (filteredMatches.length == 1) { - return join(dir.path, filteredMatches[0]); - } else if (filteredMatches.length > 1) { - throw new Error( - `More than one module matches. Use the '--skip-import' option to skip importing ` + - 'the component into the closest module or use the module option to specify a module.' - ); - } - - dir = dir.parent; - } - - const errorMsg = foundRoutingModule - ? 'Could not find a non Routing NgModule.' + - `\nModules with suffix '${routingModuleExt}' are strictly reserved for routing.` + - `\nUse the '--skip-import' option to skip importing in NgModule.` - : `Could not find an NgModule. Use the '--skip-import' option to skip importing in NgModule.`; - - throw new Error(errorMsg); -} - -/** - * Build a relative path from one file path to another file path. - */ -export function buildRelativePath(from: string, to: string): string { - from = normalize(from); - to = normalize(to); - - // Convert to arrays. - const fromParts = from.split('/'); - const toParts = to.split('/'); - - // Remove file names (preserving destination) - fromParts.pop(); - const toFileName = toParts.pop(); - - const relativePath = relative( - normalize(fromParts.join('/') || '/'), - normalize(toParts.join('/') || '/') - ); - let pathPrefix = ''; - - // Set the path prefix for same dir or child dir, parent dir starts with `..` - if (!relativePath) { - pathPrefix = '.'; - } else if (!relativePath.startsWith('.')) { - pathPrefix = `./`; - } - if (pathPrefix && !pathPrefix.endsWith('/')) { - pathPrefix += '/'; - } - - return pathPrefix + (relativePath ? relativePath + '/' : '') + toFileName; -} diff --git a/libs/transloco-schematics/src/utils/ng-schematics-utils/generate-from-files.ts b/libs/transloco-schematics/src/utils/ng-schematics-utils/generate-from-files.ts deleted file mode 100644 index aaf093999..000000000 --- a/libs/transloco-schematics/src/utils/ng-schematics-utils/generate-from-files.ts +++ /dev/null @@ -1,70 +0,0 @@ -/** - * @license - * 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 - */ - -import { - Rule, - Tree, - apply, - applyTemplates, - chain, - filter, - mergeWith, - move, - noop, - strings, - url, -} from '@angular-devkit/schematics'; - -import { parseName } from './parse-name'; -import { validateClassName } from './validation'; -import { createDefaultPath } from './workspace'; - -export interface GenerateFromFilesOptions { - flat?: boolean; - name: string; - path?: string; - prefix?: string; - project: string; - skipTests?: boolean; - templateFilesDirectory?: string; -} - -export function generateFromFiles( - options: GenerateFromFilesOptions, - extraTemplateValues: Record string)> = {} -): Rule { - return async (host: Tree) => { - options.path ??= await createDefaultPath(host, options.project); - options.prefix ??= ''; - options.flat ??= true; - - const parsedPath = parseName(options.path, options.name); - options.name = parsedPath.name; - options.path = parsedPath.path; - - validateClassName(strings.classify(options.name)); - - const templateFilesDirectory = options.templateFilesDirectory ?? './files'; - const templateSource = apply(url(templateFilesDirectory), [ - options.skipTests - ? filter((path) => !path.endsWith('.spec.ts.template')) - : noop(), - applyTemplates({ - ...strings, - ...options, - ...extraTemplateValues, - }), - move( - parsedPath.path + - (options.flat ? '' : '/' + strings.dasherize(options.name)) - ), - ]); - - return chain([mergeWith(templateSource)]); - }; -} diff --git a/libs/transloco-schematics/src/utils/ng-schematics-utils/json-file.ts b/libs/transloco-schematics/src/utils/ng-schematics-utils/json-file.ts deleted file mode 100644 index 2560d95b3..000000000 --- a/libs/transloco-schematics/src/utils/ng-schematics-utils/json-file.ts +++ /dev/null @@ -1,104 +0,0 @@ -/** - * @license - * 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 - */ - -import { JsonValue } from '@angular-devkit/core'; -import { Tree } from '@angular-devkit/schematics'; -import { - Node, - ParseError, - applyEdits, - findNodeAtLocation, - getNodeValue, - modify, - parseTree, - printParseErrorCode, -} from 'jsonc-parser'; - -export type InsertionIndex = (properties: string[]) => number; -export type JSONPath = (string | number)[]; - -/** @private */ -export class JSONFile { - content: string; - - constructor(private readonly host: Tree, private readonly path: string) { - this.content = this.host.readText(this.path); - } - - private _jsonAst: Node | undefined; - private get JsonAst(): Node | undefined { - if (this._jsonAst) { - return this._jsonAst; - } - - const errors: ParseError[] = []; - this._jsonAst = parseTree(this.content, errors, { - allowTrailingComma: true, - }); - if (errors.length) { - const { error, offset } = errors[0]; - throw new Error( - `Failed to parse "${ - this.path - }" as JSON AST Object. ${printParseErrorCode( - error - )} at location: ${offset}.` - ); - } - - return this._jsonAst; - } - - get(jsonPath: JSONPath): unknown { - const jsonAstNode = this.JsonAst; - if (!jsonAstNode) { - return undefined; - } - - if (jsonPath.length === 0) { - return getNodeValue(jsonAstNode); - } - - const node = findNodeAtLocation(jsonAstNode, jsonPath); - - return node === undefined ? undefined : getNodeValue(node); - } - - modify( - jsonPath: JSONPath, - value: JsonValue | undefined, - insertInOrder?: InsertionIndex | false - ): void { - let getInsertionIndex: InsertionIndex | undefined; - if (insertInOrder === undefined) { - const property = jsonPath.slice(-1)[0]; - getInsertionIndex = (properties) => - [...properties, property].sort().findIndex((p) => p === property); - } else if (insertInOrder !== false) { - getInsertionIndex = insertInOrder; - } - - const edits = modify(this.content, jsonPath, value, { - getInsertionIndex, - formattingOptions: { - insertSpaces: true, - tabSize: 2, - }, - }); - - this.content = applyEdits(this.content, edits); - this.host.overwrite(this.path, this.content); - this._jsonAst = undefined; - } - - remove(jsonPath: JSONPath): void { - if (this.get(jsonPath) !== undefined) { - this.modify(jsonPath, undefined); - } - } -} diff --git a/libs/transloco-schematics/src/utils/ng-schematics-utils/ng-ast-utils.ts b/libs/transloco-schematics/src/utils/ng-schematics-utils/ng-ast-utils.ts deleted file mode 100644 index 44d7e73c4..000000000 --- a/libs/transloco-schematics/src/utils/ng-schematics-utils/ng-ast-utils.ts +++ /dev/null @@ -1,110 +0,0 @@ -/** - * @license - * 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 - */ - -import { dirname } from 'node:path'; - -import { normalize } from '@angular-devkit/core'; -import { SchematicsException, Tree } from '@angular-devkit/schematics'; -import * as ts from 'typescript'; - -import { findBootstrapApplicationCall } from './standalone/util'; -import { findNode, getSourceNodes } from './ast-utils'; - -export function findBootstrapModuleCall( - host: Tree, - mainPath: string -): ts.CallExpression | null { - const mainText = host.readText(mainPath); - const source = ts.createSourceFile( - mainPath, - mainText, - ts.ScriptTarget.Latest, - true - ); - - const allNodes = getSourceNodes(source); - - let bootstrapCall: ts.CallExpression | null = null; - - for (const node of allNodes) { - let bootstrapCallNode: ts.Node | null = null; - bootstrapCallNode = findNode( - node, - ts.SyntaxKind.Identifier, - 'bootstrapModule' - ); - - // Walk up the parent until CallExpression is found. - while ( - bootstrapCallNode && - bootstrapCallNode.parent && - bootstrapCallNode.parent.kind !== ts.SyntaxKind.CallExpression - ) { - bootstrapCallNode = bootstrapCallNode.parent; - } - - if ( - bootstrapCallNode !== null && - bootstrapCallNode.parent !== undefined && - bootstrapCallNode.parent.kind === ts.SyntaxKind.CallExpression - ) { - bootstrapCall = bootstrapCallNode.parent as ts.CallExpression; - break; - } - } - - return bootstrapCall; -} - -function findBootstrapModulePath(host: Tree, mainPath: string): string { - const bootstrapCall = findBootstrapModuleCall(host, mainPath); - if (!bootstrapCall) { - throw new SchematicsException('Bootstrap call not found'); - } - - const bootstrapModule = bootstrapCall.arguments[0]; - - const mainText = host.readText(mainPath); - const source = ts.createSourceFile( - mainPath, - mainText, - ts.ScriptTarget.Latest, - true - ); - const allNodes = getSourceNodes(source); - const bootstrapModuleRelativePath = allNodes - .filter(ts.isImportDeclaration) - .filter((imp) => { - return findNode(imp, ts.SyntaxKind.Identifier, bootstrapModule.getText()); - }) - .map((imp) => { - const modulePathStringLiteral = imp.moduleSpecifier as ts.StringLiteral; - - return modulePathStringLiteral.text; - })[0]; - - return bootstrapModuleRelativePath; -} - -export function getAppModulePath(host: Tree, mainPath: string): string { - const moduleRelativePath = findBootstrapModulePath(host, mainPath); - const mainDir = dirname(mainPath); - const modulePath = normalize(`/${mainDir}/${moduleRelativePath}.ts`); - - return modulePath; -} - -export function isStandaloneApp(host: Tree, mainPath: string): boolean { - let bootstrapCall = null; - try { - bootstrapCall = findBootstrapApplicationCall(host, mainPath); - // eslint-disable-next-line no-empty - } catch {} - - return bootstrapCall !== null; -} diff --git a/libs/transloco-schematics/src/utils/ng-schematics-utils/parse-name.ts b/libs/transloco-schematics/src/utils/ng-schematics-utils/parse-name.ts deleted file mode 100644 index 01227198c..000000000 --- a/libs/transloco-schematics/src/utils/ng-schematics-utils/parse-name.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** - * @license - * 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 - */ - -import { Path, basename, dirname, join, normalize } from '@angular-devkit/core'; - -export interface Location { - name: string; - path: Path; -} - -export function parseName(path: string, name: string): Location { - const nameWithoutPath = basename(normalize(name)); - const namePath = dirname(join(normalize(path), name)); - - return { - name: nameWithoutPath, - path: normalize('/' + namePath), - }; -} diff --git a/libs/transloco-schematics/src/utils/ng-schematics-utils/paths.ts b/libs/transloco-schematics/src/utils/ng-schematics-utils/paths.ts deleted file mode 100644 index 8f7ced59e..000000000 --- a/libs/transloco-schematics/src/utils/ng-schematics-utils/paths.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * @license - * 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 - */ - -import { normalize, split } from '@angular-devkit/core'; - -export function relativePathToWorkspaceRoot( - projectRoot: string | undefined -): string { - const normalizedPath = split(normalize(projectRoot || '')); - - if (normalizedPath.length === 0 || !normalizedPath[0]) { - return '.'; - } else { - return normalizedPath.map(() => '..').join('/'); - } -} diff --git a/libs/transloco-schematics/src/utils/ng-schematics-utils/project-targets.ts b/libs/transloco-schematics/src/utils/ng-schematics-utils/project-targets.ts deleted file mode 100644 index 7f4b7ba8c..000000000 --- a/libs/transloco-schematics/src/utils/ng-schematics-utils/project-targets.ts +++ /dev/null @@ -1,13 +0,0 @@ -/** - * @license - * 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 - */ - -import { SchematicsException } from '@angular-devkit/schematics'; - -export function targetBuildNotFoundError(): SchematicsException { - return new SchematicsException(`Project target "build" not found.`); -} diff --git a/libs/transloco-schematics/src/utils/ng-schematics-utils/standalone/app_config.ts b/libs/transloco-schematics/src/utils/ng-schematics-utils/standalone/app_config.ts deleted file mode 100644 index a8035b23b..000000000 --- a/libs/transloco-schematics/src/utils/ng-schematics-utils/standalone/app_config.ts +++ /dev/null @@ -1,137 +0,0 @@ -/** - * @license - * 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 - */ - -import { dirname, join } from 'node:path'; - -import { Tree } from '@angular-devkit/schematics'; -import * as ts from 'typescript'; - -import { getSourceFile } from './util'; - -/** App config that was resolved to its source node. */ -export interface ResolvedAppConfig { - /** Tree-relative path of the file containing the app config. */ - filePath: string; - - /** Node defining the app config. */ - node: ts.ObjectLiteralExpression; -} - -/** - * Resolves the node that defines the app config from a bootstrap call. - * @param bootstrapCall Call for which to resolve the config. - * @param tree File tree of the project. - * @param filePath File path of the bootstrap call. - */ -export function findAppConfig( - bootstrapCall: ts.CallExpression, - tree: Tree, - filePath: string -): ResolvedAppConfig | null { - if (bootstrapCall.arguments.length > 1) { - const config = bootstrapCall.arguments[1]; - - if (ts.isObjectLiteralExpression(config)) { - return { filePath, node: config }; - } - - if (ts.isIdentifier(config)) { - return resolveAppConfigFromIdentifier(config, tree, filePath); - } - } - - return null; -} - -/** - * Resolves the app config from an identifier referring to it. - * @param identifier Identifier referring to the app config. - * @param tree File tree of the project. - * @param bootstapFilePath Path of the bootstrap call. - */ -function resolveAppConfigFromIdentifier( - identifier: ts.Identifier, - tree: Tree, - bootstapFilePath: string -): ResolvedAppConfig | null { - const sourceFile = identifier.getSourceFile(); - - for (const node of sourceFile.statements) { - // Only look at relative imports. This will break if the app uses a path - // mapping to refer to the import, but in order to resolve those, we would - // need knowledge about the entire program. - if ( - !ts.isImportDeclaration(node) || - !node.importClause?.namedBindings || - !ts.isNamedImports(node.importClause.namedBindings) || - !ts.isStringLiteralLike(node.moduleSpecifier) || - !node.moduleSpecifier.text.startsWith('.') - ) { - continue; - } - - for (const specifier of node.importClause.namedBindings.elements) { - if (specifier.name.text !== identifier.text) { - continue; - } - - // Look for a variable with the imported name in the file. Note that ideally we would use - // the type checker to resolve this, but we can't because these utilities are set up to - // operate on individual files, not the entire program. - const filePath = join( - dirname(bootstapFilePath), - node.moduleSpecifier.text + '.ts' - ); - const importedSourceFile = getSourceFile(tree, filePath); - const resolvedVariable = findAppConfigFromVariableName( - importedSourceFile, - (specifier.propertyName || specifier.name).text - ); - - if (resolvedVariable) { - return { filePath, node: resolvedVariable }; - } - } - } - - const variableInSameFile = findAppConfigFromVariableName( - sourceFile, - identifier.text - ); - - return variableInSameFile - ? { filePath: bootstapFilePath, node: variableInSameFile } - : null; -} - -/** - * Finds an app config within the top-level variables of a file. - * @param sourceFile File in which to search for the config. - * @param variableName Name of the variable containing the config. - */ -function findAppConfigFromVariableName( - sourceFile: ts.SourceFile, - variableName: string -): ts.ObjectLiteralExpression | null { - for (const node of sourceFile.statements) { - if (ts.isVariableStatement(node)) { - for (const decl of node.declarationList.declarations) { - if ( - ts.isIdentifier(decl.name) && - decl.name.text === variableName && - decl.initializer && - ts.isObjectLiteralExpression(decl.initializer) - ) { - return decl.initializer; - } - } - } - } - - return null; -} diff --git a/libs/transloco-schematics/src/utils/ng-schematics-utils/standalone/code_block.ts b/libs/transloco-schematics/src/utils/ng-schematics-utils/standalone/code_block.ts deleted file mode 100644 index b13094888..000000000 --- a/libs/transloco-schematics/src/utils/ng-schematics-utils/standalone/code_block.ts +++ /dev/null @@ -1,135 +0,0 @@ -/** - * @license - * 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 - */ - -import { Rule, Tree } from '@angular-devkit/schematics'; -import * as ts from 'typescript'; - -import { hasTopLevelIdentifier, insertImport } from '../ast-utils'; -import { applyToUpdateRecorder } from '../change'; - -/** Generated code that hasn't been interpolated yet. */ -export interface PendingCode { - /** Code that will be inserted. */ - expression: string; - - /** Imports that need to be added to the file in which the code is inserted. */ - imports: PendingImports; -} - -/** Map keeping track of imports and aliases under which they're referred to in an expresion. */ -type PendingImports = Map>; - -/** Counter used to generate unique IDs. */ -let uniqueIdCounter = 0; - -/** - * Callback invoked by a Rule that produces the code - * that needs to be inserted somewhere in the app. - */ -export type CodeBlockCallback = (block: CodeBlock) => PendingCode; - -/** - * Utility class used to generate blocks of code that - * can be inserted by the devkit into a user's app. - */ -export class CodeBlock { - private _imports: PendingImports = new Map>(); - - // Note: the methods here are defined as arrow function so that they can be destructured by - // consumers without losing their context. This makes the API more concise. - - /** Function used to tag a code block in order to produce a `PendingCode` object. */ - code = (strings: TemplateStringsArray, ...params: unknown[]): PendingCode => { - return { - expression: strings - .map((part, index) => part + (params[index] || '')) - .join(''), - imports: this._imports, - }; - }; - - /** - * Used inside of a code block to mark external symbols and which module they should be imported - * from. When the code is inserted, the required import statements will be produced automatically. - * @param symbolName Name of the external symbol. - * @param moduleName Module from which the symbol should be imported. - */ - external = (symbolName: string, moduleName: string): string => { - if (!this._imports.has(moduleName)) { - this._imports.set(moduleName, new Map()); - } - - const symbolsPerModule = this._imports.get(moduleName) as Map< - string, - string - >; - - if (!symbolsPerModule.has(symbolName)) { - symbolsPerModule.set( - symbolName, - `@@__SCHEMATIC_PLACEHOLDER_${uniqueIdCounter++}__@@` - ); - } - - return symbolsPerModule.get(symbolName) as string; - }; - - /** - * Produces the necessary rules to transform a `PendingCode` object into valid code. - * @param initialCode Code pending transformed. - * @param filePath Path of the file in which the code will be inserted. - */ - static transformPendingCode(initialCode: PendingCode, filePath: string) { - const code = { ...initialCode }; - const rules: Rule[] = []; - - code.imports.forEach((symbols, moduleName) => { - symbols.forEach((placeholder, symbolName) => { - rules.push((tree: Tree) => { - const recorder = tree.beginUpdate(filePath); - const sourceFile = ts.createSourceFile( - filePath, - tree.readText(filePath), - ts.ScriptTarget.Latest, - true - ); - - // Note that this could still technically clash if there's a top-level symbol called - // `${symbolName}_alias`, however this is unlikely. We can revisit this if it becomes - // a problem. - const alias = hasTopLevelIdentifier( - sourceFile, - symbolName, - moduleName - ) - ? symbolName + '_alias' - : undefined; - - code.expression = code.expression.replace( - new RegExp(placeholder, 'g'), - alias || symbolName - ); - - applyToUpdateRecorder(recorder, [ - insertImport( - sourceFile, - filePath, - symbolName, - moduleName, - false, - alias - ), - ]); - tree.commitUpdate(recorder); - }); - }); - }); - - return { code, rules }; - } -} diff --git a/libs/transloco-schematics/src/utils/ng-schematics-utils/standalone/index.ts b/libs/transloco-schematics/src/utils/ng-schematics-utils/standalone/index.ts deleted file mode 100644 index 1615b02b8..000000000 --- a/libs/transloco-schematics/src/utils/ng-schematics-utils/standalone/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * @license - * 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 - */ - -export { addRootImport, addRootProvider } from './rules'; -export { PendingCode, CodeBlockCallback, type CodeBlock } from './code_block'; diff --git a/libs/transloco-schematics/src/utils/ng-schematics-utils/standalone/rules.ts b/libs/transloco-schematics/src/utils/ng-schematics-utils/standalone/rules.ts deleted file mode 100644 index 52fbe9a75..000000000 --- a/libs/transloco-schematics/src/utils/ng-schematics-utils/standalone/rules.ts +++ /dev/null @@ -1,297 +0,0 @@ -/** - * @license - * 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 - */ - -import { tags } from '@angular-devkit/core'; -import { - Rule, - SchematicsException, - Tree, - chain, -} from '@angular-devkit/schematics'; -import * as ts from 'typescript'; - -import { - addSymbolToNgModuleMetadata, - insertAfterLastOccurrence, -} from '../ast-utils'; -import { InsertChange } from '../change'; -import { getAppModulePath, isStandaloneApp } from '../ng-ast-utils'; - -import { ResolvedAppConfig, findAppConfig } from './app_config'; -import { CodeBlock, CodeBlockCallback, PendingCode } from './code_block'; -import { - applyChangesToFile, - findBootstrapApplicationCall, - findProvidersLiteral, - getMainFilePath, - getSourceFile, - isMergeAppConfigCall, -} from './util'; - -/** - * Adds an import to the root of the project. - * @param project Name of the project to which to add the import. - * @param callback Function that generates the code block which should be inserted. - * @example - * - * ```ts - * import { Rule } from '@angular-devkit/schematics'; - * import { addRootImport } from '@schematics/angular/utility'; - * - * export default function(): Rule { - * return addRootImport('default', ({code, external}) => { - * return code`${external('MyModule', '@my/module')}.forRoot({})`; - * }); - * } - * ``` - */ -export function addRootImport( - project: string, - callback: CodeBlockCallback -): Rule { - return getRootInsertionRule(project, callback, 'imports', { - name: 'importProvidersFrom', - module: '@angular/core', - }); -} - -/** - * Adds a provider to the root of the project. - * @param project Name of the project to which to add the import. - * @param callback Function that generates the code block which should be inserted. - * @example - * - * ```ts - * import { Rule } from '@angular-devkit/schematics'; - * import { addRootProvider } from '@schematics/angular/utility'; - * - * export default function(): Rule { - * return addRootProvider('default', ({code, external}) => { - * return code`${external('provideLibrary', '@my/library')}({})`; - * }); - * } - * ``` - */ -export function addRootProvider( - project: string, - callback: CodeBlockCallback -): Rule { - return getRootInsertionRule(project, callback, 'providers'); -} - -/** - * Creates a rule that inserts code at the root of either a standalone or NgModule-based project. - * @param project Name of the project into which to insert the code. - * @param callback Function that generates the code block which should be inserted. - * @param ngModuleField Field of the root NgModule into which the code should be inserted, if the - * app is based on NgModule - * @param standaloneWrapperFunction Function with which to wrap the code if the app is standalone. - */ -function getRootInsertionRule( - project: string, - callback: CodeBlockCallback, - ngModuleField: string, - standaloneWrapperFunction?: { name: string; module: string } -): Rule { - return async (host) => { - const mainFilePath = await getMainFilePath(host, project); - const codeBlock = new CodeBlock(); - - if (isStandaloneApp(host, mainFilePath)) { - return (tree) => - addProviderToStandaloneBootstrap( - tree, - callback(codeBlock), - mainFilePath, - standaloneWrapperFunction - ); - } - - const modulePath = getAppModulePath(host, mainFilePath); - const pendingCode = CodeBlock.transformPendingCode( - callback(codeBlock), - modulePath - ); - - return chain([ - ...pendingCode.rules, - (tree) => { - const changes = addSymbolToNgModuleMetadata( - getSourceFile(tree, modulePath), - modulePath, - ngModuleField, - pendingCode.code.expression, - // Explicitly set the import path to null since we deal with imports here separately. - null - ); - - applyChangesToFile(tree, modulePath, changes); - }, - ]); - }; -} - -/** - * Adds a provider to the root of a standalone project. - * @param host Tree of the root rule. - * @param pendingCode Code that should be inserted. - * @param mainFilePath Path to the project's main file. - * @param wrapperFunction Optional function with which to wrap the provider. - */ -function addProviderToStandaloneBootstrap( - host: Tree, - pendingCode: PendingCode, - mainFilePath: string, - wrapperFunction?: { name: string; module: string } -): Rule { - const bootstrapCall = findBootstrapApplicationCall(host, mainFilePath); - const fileToEdit = - findAppConfig(bootstrapCall, host, mainFilePath)?.filePath || mainFilePath; - const { code, rules } = CodeBlock.transformPendingCode( - pendingCode, - fileToEdit - ); - - return chain([ - ...rules, - () => { - let wrapped: PendingCode; - let additionalRules: Rule[]; - - if (wrapperFunction) { - const block = new CodeBlock(); - const result = CodeBlock.transformPendingCode( - block.code`${block.external( - wrapperFunction.name, - wrapperFunction.module - )}(${code.expression})`, - fileToEdit - ); - - wrapped = result.code; - additionalRules = result.rules; - } else { - wrapped = code; - additionalRules = []; - } - - return chain([ - ...additionalRules, - (tree) => - insertStandaloneRootProvider(tree, mainFilePath, wrapped.expression), - ]); - }, - ]); -} - -/** - * Inserts a string expression into the root of a standalone project. - * @param tree File tree used to modify the project. - * @param mainFilePath Path to the main file of the project. - * @param expression Code expression to be inserted. - */ -function insertStandaloneRootProvider( - tree: Tree, - mainFilePath: string, - expression: string -): void { - const bootstrapCall = findBootstrapApplicationCall(tree, mainFilePath); - const appConfig = findAppConfig(bootstrapCall, tree, mainFilePath); - - if (bootstrapCall.arguments.length === 0) { - throw new SchematicsException( - `Cannot add provider to invalid bootstrapApplication call in ${ - bootstrapCall.getSourceFile().fileName - }` - ); - } - - if (appConfig) { - addProvidersExpressionToAppConfig(tree, appConfig, expression); - - return; - } - - const newAppConfig = `, {\n${tags.indentBy( - 2 - )`providers: [${expression}]`}\n}`; - let targetCall: ts.CallExpression; - - if (bootstrapCall.arguments.length === 1) { - targetCall = bootstrapCall; - } else if (isMergeAppConfigCall(bootstrapCall.arguments[1])) { - targetCall = bootstrapCall.arguments[1]; - } else { - throw new SchematicsException( - `Cannot statically analyze bootstrapApplication call in ${ - bootstrapCall.getSourceFile().fileName - }` - ); - } - - applyChangesToFile(tree, mainFilePath, [ - insertAfterLastOccurrence( - targetCall.arguments, - newAppConfig, - mainFilePath, - targetCall.getEnd() - 1 - ), - ]); -} - -/** - * Adds a string expression to an app config object. - * @param tree File tree used to modify the project. - * @param appConfig Resolved configuration object of the project. - * @param expression Code expression to be inserted. - */ -function addProvidersExpressionToAppConfig( - tree: Tree, - appConfig: ResolvedAppConfig, - expression: string -): void { - const { node, filePath } = appConfig; - const configProps = node.properties; - const providersLiteral = findProvidersLiteral(node); - - // If there's a `providers` property, we can add the provider - // to it, otherwise we need to declare it ourselves. - if (providersLiteral) { - const hasTrailingComma = providersLiteral.elements.hasTrailingComma; - - applyChangesToFile(tree, filePath, [ - insertAfterLastOccurrence( - providersLiteral.elements, - (hasTrailingComma || providersLiteral.elements.length === 0 - ? '' - : ', ') + expression, - filePath, - providersLiteral.getStart() + 1 - ), - ]); - } else { - const prop = tags.indentBy(2)`providers: [${expression}]`; - let toInsert: string; - let insertPosition: number; - - if (configProps.length === 0) { - toInsert = '\n' + prop + '\n'; - insertPosition = node.getEnd() - 1; - } else { - const hasTrailingComma = configProps.hasTrailingComma; - toInsert = (hasTrailingComma ? '' : ',') + '\n' + prop; - insertPosition = - configProps[configProps.length - 1].getEnd() + - (hasTrailingComma ? 1 : 0); - } - - applyChangesToFile(tree, filePath, [ - new InsertChange(filePath, insertPosition, toInsert), - ]); - } -} diff --git a/libs/transloco-schematics/src/utils/ng-schematics-utils/standalone/util.ts b/libs/transloco-schematics/src/utils/ng-schematics-utils/standalone/util.ts deleted file mode 100644 index 3c2fee6a3..000000000 --- a/libs/transloco-schematics/src/utils/ng-schematics-utils/standalone/util.ts +++ /dev/null @@ -1,188 +0,0 @@ -/** - * @license - * 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 - */ - -import { SchematicsException, Tree } from '@angular-devkit/schematics'; -import * as ts from 'typescript'; - -import { Change, applyToUpdateRecorder } from '../change'; -import { targetBuildNotFoundError } from '../project-targets'; -import { getWorkspace } from '../workspace'; -import { BrowserBuilderOptions } from '../workspace-models'; - -/** - * Finds the main file of a project. - * @param tree File tree for the project. - * @param projectName Name of the project in which to search. - */ -export async function getMainFilePath( - tree: Tree, - projectName: string -): Promise { - const workspace = await getWorkspace(tree); - const project = workspace.projects.get(projectName); - const buildTarget = project?.targets.get('build'); - - if (!buildTarget) { - throw targetBuildNotFoundError(); - } - - return ((buildTarget.options || {}) as unknown as BrowserBuilderOptions).main; -} - -/** - * Gets a TypeScript source file at a specific path. - * @param tree File tree of a project. - * @param path Path to the file. - */ -export function getSourceFile(tree: Tree, path: string): ts.SourceFile { - const content = tree.readText(path); - const source = ts.createSourceFile( - path, - content, - ts.ScriptTarget.Latest, - true - ); - - return source; -} - -/** Finds the call to `bootstrapApplication` within a file. */ -export function findBootstrapApplicationCall( - tree: Tree, - mainFilePath: string -): ts.CallExpression { - const sourceFile = getSourceFile(tree, mainFilePath); - const localName = findImportLocalName( - sourceFile, - 'bootstrapApplication', - '@angular/platform-browser' - ); - - if (localName) { - let result: ts.CallExpression | null = null; - - sourceFile.forEachChild(function walk(node) { - if ( - ts.isCallExpression(node) && - ts.isIdentifier(node.expression) && - node.expression.text === localName - ) { - result = node; - } - - if (!result) { - node.forEachChild(walk); - } - }); - - if (result) { - return result; - } - } - - throw new SchematicsException( - `Could not find bootstrapApplication call in ${mainFilePath}` - ); -} - -/** - * Finds the local name of an imported symbol. Could be the symbol name itself or its alias. - * @param sourceFile File within which to search for the import. - * @param name Actual name of the import, not its local alias. - * @param moduleName Name of the module from which the symbol is imported. - */ -function findImportLocalName( - sourceFile: ts.SourceFile, - name: string, - moduleName: string -): string | null { - for (const node of sourceFile.statements) { - // Only look for top-level imports. - if ( - !ts.isImportDeclaration(node) || - !ts.isStringLiteral(node.moduleSpecifier) || - node.moduleSpecifier.text !== moduleName - ) { - continue; - } - - // Filter out imports that don't have the right shape. - if ( - !node.importClause || - !node.importClause.namedBindings || - !ts.isNamedImports(node.importClause.namedBindings) - ) { - continue; - } - - // Look through the elements of the declaration for the specific import. - for (const element of node.importClause.namedBindings.elements) { - if ((element.propertyName || element.name).text === name) { - // The local name is always in `name`. - return element.name.text; - } - } - } - - return null; -} - -/** - * Applies a set of changes to a file. - * @param tree File tree of the project. - * @param path Path to the file that is being changed. - * @param changes Changes that should be applied to the file. - */ -export function applyChangesToFile( - tree: Tree, - path: string, - changes: Change[] -) { - if (changes.length > 0) { - const recorder = tree.beginUpdate(path); - applyToUpdateRecorder(recorder, changes); - tree.commitUpdate(recorder); - } -} - -/** Checks whether a node is a call to `mergeApplicationConfig`. */ -export function isMergeAppConfigCall(node: ts.Node): node is ts.CallExpression { - if (!ts.isCallExpression(node)) { - return false; - } - - const localName = findImportLocalName( - node.getSourceFile(), - 'mergeApplicationConfig', - '@angular/core' - ); - - return ( - !!localName && - ts.isIdentifier(node.expression) && - node.expression.text === localName - ); -} - -/** Finds the `providers` array literal within an application config. */ -export function findProvidersLiteral( - config: ts.ObjectLiteralExpression -): ts.ArrayLiteralExpression | null { - for (const prop of config.properties) { - if ( - ts.isPropertyAssignment(prop) && - ts.isIdentifier(prop.name) && - prop.name.text === 'providers' && - ts.isArrayLiteralExpression(prop.initializer) - ) { - return prop.initializer; - } - } - - return null; -} diff --git a/libs/transloco-schematics/src/utils/ng-schematics-utils/validation.ts b/libs/transloco-schematics/src/utils/ng-schematics-utils/validation.ts deleted file mode 100644 index c2779fd89..000000000 --- a/libs/transloco-schematics/src/utils/ng-schematics-utils/validation.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** - * @license - * 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 - */ - -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]*)*$/; - -// 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; - -export function validateHtmlSelector(selector: string): void { - if (selector && !htmlSelectorRe.test(selector)) { - throw new SchematicsException(`Selector "${selector}" is invalid.`); - } -} - -export function validateClassName(className: string): void { - if (!ecmaIdentifierNameRegExp.test(className)) { - throw new SchematicsException(`Class name "${className}" is invalid.`); - } -} diff --git a/libs/transloco-schematics/src/utils/ng-schematics-utils/workspace-models.ts b/libs/transloco-schematics/src/utils/ng-schematics-utils/workspace-models.ts deleted file mode 100644 index 17ecb0cde..000000000 --- a/libs/transloco-schematics/src/utils/ng-schematics-utils/workspace-models.ts +++ /dev/null @@ -1,212 +0,0 @@ -/** - * @license - * 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 - */ - -export enum ProjectType { - Application = 'application', - Library = 'library', -} - -/** - * An enum of the official Angular builders. - * Each enum value provides the fully qualified name of the associated builder. - * This enum can be used when analyzing the `builder` fields of project configurations from the - * `angular.json` workspace file. - */ -export enum Builders { - AppShell = '@angular-devkit/build-angular:app-shell', - Server = '@angular-devkit/build-angular:server', - Browser = '@angular-devkit/build-angular:browser', - Karma = '@angular-devkit/build-angular:karma', - TsLint = '@angular-devkit/build-angular:tslint', - DeprecatedNgPackagr = '@angular-devkit/build-ng-packagr:build', - NgPackagr = '@angular-devkit/build-angular:ng-packagr', - DevServer = '@angular-devkit/build-angular:dev-server', - ExtractI18n = '@angular-devkit/build-angular:extract-i18n', - Protractor = '@angular-devkit/build-angular:protractor', -} - -export interface FileReplacements { - replace: string; - with: string; -} - -export interface BrowserBuilderBaseOptions { - main: string; - tsConfig: string; - fileReplacements?: FileReplacements[]; - outputPath?: string; - index?: string; - polyfills: string; - assets?: (object | string)[]; - styles?: (object | string)[]; - scripts?: (object | string)[]; - sourceMap?: boolean; -} - -export type OutputHashing = 'all' | 'media' | 'none' | 'bundles'; - -export interface BrowserBuilderOptions extends BrowserBuilderBaseOptions { - serviceWorker?: boolean; - optimization?: boolean; - outputHashing?: OutputHashing; - resourcesOutputPath?: string; - namedChunks?: boolean; - aot?: boolean; - extractLicenses?: boolean; - vendorChunk?: boolean; - buildOptimizer?: boolean; - ngswConfigPath?: string; - budgets?: { - type: string; - maximumWarning?: string; - maximumError?: string; - }[]; - webWorkerTsConfig?: string; -} - -export interface ServeBuilderOptions { - browserTarget: string; -} -export interface LibraryBuilderOptions { - tsConfig: string; - project: string; -} - -export interface ServerBuilderOptions { - outputPath: string; - tsConfig: string; - main: string; - fileReplacements?: FileReplacements[]; - optimization?: - | boolean - | { - scripts?: boolean; - styles?: boolean; - }; - sourceMap?: - | boolean - | { - scripts?: boolean; - styles?: boolean; - hidden?: boolean; - vendor?: boolean; - }; -} - -export interface AppShellBuilderOptions { - browserTarget: string; - serverTarget: string; - route: string; -} - -export interface TestBuilderOptions extends Partial { - karmaConfig: string; -} - -export interface ExtractI18nOptions { - browserTarget: string; -} - -export interface E2EOptions { - protractorConfig: string; - devServerTarget: string; -} - -export interface BuilderTarget { - builder: TBuilder; - options: TOptions; - configurations?: { - production: Partial; - [key: string]: Partial; - }; -} - -export type LibraryBuilderTarget = BuilderTarget< - Builders.NgPackagr, - LibraryBuilderOptions ->; -export type BrowserBuilderTarget = BuilderTarget< - Builders.Browser, - BrowserBuilderOptions ->; -export type ServerBuilderTarget = BuilderTarget< - Builders.Server, - ServerBuilderOptions ->; -export type AppShellBuilderTarget = BuilderTarget< - Builders.AppShell, - AppShellBuilderOptions ->; -export type TestBuilderTarget = BuilderTarget< - Builders.Karma, - TestBuilderOptions ->; -export type ServeBuilderTarget = BuilderTarget< - Builders.DevServer, - ServeBuilderOptions ->; -export type ExtractI18nBuilderTarget = BuilderTarget< - Builders.ExtractI18n, - ExtractI18nOptions ->; -export type E2EBuilderTarget = BuilderTarget; - -interface WorkspaceCLISchema { - warnings?: Record; - schematicCollections?: string[]; -} -export interface WorkspaceSchema { - version: 1; - cli?: WorkspaceCLISchema; - projects: { - [key: string]: WorkspaceProject< - ProjectType.Application | ProjectType.Library - >; - }; -} - -export interface WorkspaceProject< - TProjectType extends ProjectType = ProjectType.Application -> { - /** - * Project type. - */ - projectType: ProjectType; - - root: string; - sourceRoot: string; - prefix: string; - - cli?: WorkspaceCLISchema; - - /** - * Tool options. - */ - architect?: WorkspaceTargets; - /** - * Tool options. - */ - targets?: WorkspaceTargets; -} - -export interface WorkspaceTargets< - TProjectType extends ProjectType = ProjectType.Application -> { - build?: TProjectType extends ProjectType.Library - ? LibraryBuilderTarget - : BrowserBuilderTarget; - server?: ServerBuilderTarget; - test?: TestBuilderTarget; - serve?: ServeBuilderTarget; - e2e?: E2EBuilderTarget; - 'app-shell'?: AppShellBuilderTarget; - 'extract-i18n'?: ExtractI18nBuilderTarget; - // TODO(hans): change this any to unknown when google3 supports TypeScript 3.0. - // eslint-disable-next-line @typescript-eslint/no-explicit-any - [key: string]: any; -} diff --git a/libs/transloco-schematics/src/utils/ng-schematics-utils/workspace.ts b/libs/transloco-schematics/src/utils/ng-schematics-utils/workspace.ts deleted file mode 100644 index dbc65d8cb..000000000 --- a/libs/transloco-schematics/src/utils/ng-schematics-utils/workspace.ts +++ /dev/null @@ -1,184 +0,0 @@ -/** - * @license - * 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 - */ - -import { json, workspaces } from '@angular-devkit/core'; -import { Rule, Tree, noop } from '@angular-devkit/schematics'; - -import { ProjectType } from './workspace-models'; - -const DEFAULT_WORKSPACE_PATH = '/angular.json'; - -// re-export the workspace definition types for convenience -export type WorkspaceDefinition = workspaces.WorkspaceDefinition; -export type ProjectDefinition = workspaces.ProjectDefinition; -export type TargetDefinition = workspaces.TargetDefinition; - -/** - * A {@link workspaces.WorkspaceHost} backed by a Schematics {@link Tree} instance. - */ -class TreeWorkspaceHost implements workspaces.WorkspaceHost { - constructor(private readonly tree: Tree) {} - - async readFile(path: string): Promise { - return this.tree.readText(path); - } - - async writeFile(path: string, data: string): Promise { - if (this.tree.exists(path)) { - this.tree.overwrite(path, data); - } else { - this.tree.create(path, data); - } - } - - async isDirectory(path: string): Promise { - // approximate a directory check - return ( - !this.tree.exists(path) && this.tree.getDir(path).subfiles.length > 0 - ); - } - - async isFile(path: string): Promise { - return this.tree.exists(path); - } -} - -/** - * Updates the workspace file (`angular.json`) found within the root of the schematic's tree. - * The workspace object model can be directly modified within the provided updater function - * with changes being written to the workspace file after the updater function returns. - * The spacing and overall layout of the file (including comments) will be maintained where - * possible when updating the file. - * - * @param updater An update function that can be used to modify the object model for the - * workspace. A {@link WorkspaceDefinition} is provided as the first argument to the function. - */ -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 - ); - - const result = await updater(workspace); - - await workspaces.writeWorkspace(workspace, host); - - return result || noop; - }; -} - -// TODO: This should be renamed `readWorkspace` once deep imports are restricted (already exported from `utility` with that name) -/** - * Reads a workspace file (`angular.json`) from the provided {@link Tree} instance. - * - * @param tree A schematics {@link Tree} instance used to access the workspace file. - * @param path The path where a workspace file should be found. If a file is specified, the file - * path will be used. If a directory is specified, the file `angular.json` will be used from - * within the specified directory. Defaults to `/angular.json`. - * @returns A {@link WorkspaceDefinition} representing the workspace found at the specified path. - */ -export async function getWorkspace( - tree: Tree, - path = DEFAULT_WORKSPACE_PATH -): Promise { - const host = new TreeWorkspaceHost(tree); - - const { workspace } = await workspaces.readWorkspace(path, host); - - return workspace; -} - -/** - * Writes a workspace file (`angular.json`) to the provided {@link Tree} instance. - * The spacing and overall layout of an exisitng file (including comments) will be maintained where - * possible when writing the file. - * - * @param tree A schematics {@link Tree} instance used to access the workspace file. - * @param workspace The {@link WorkspaceDefinition} to write. - * @param path The path where a workspace file should be written. If a file is specified, the file - * path will be used. If not provided, the definition's underlying file path stored during reading - * will be used. - */ -export async function writeWorkspace( - tree: Tree, - workspace: WorkspaceDefinition, - path?: string -): Promise { - const host = new TreeWorkspaceHost(tree); - - return workspaces.writeWorkspace(workspace, host, path); -} - -/** - * Build a default project path for generating. - * @param project The project which will have its default path generated. - */ -export function buildDefaultPath( - project: workspaces.ProjectDefinition -): string { - const root = project.sourceRoot - ? `/${project.sourceRoot}/` - : `/${project.root}/src/`; - const projectDirName = - project.extensions['projectType'] === ProjectType.Application - ? 'app' - : 'lib'; - - return `${root}${projectDirName}`; -} - -export async function createDefaultPath( - tree: Tree, - projectName: string -): Promise { - const workspace = await getWorkspace(tree); - const project = workspace.projects.get(projectName); - if (!project) { - throw new Error(`Project "${projectName}" does not exist.`); - } - - return buildDefaultPath(project); -} - -export function* allWorkspaceTargets( - workspace: workspaces.WorkspaceDefinition -): Iterable< - [string, workspaces.TargetDefinition, string, workspaces.ProjectDefinition] -> { - for (const [projectName, project] of workspace.projects) { - for (const [targetName, target] of project.targets) { - yield [targetName, target, projectName, project]; - } - } -} - -export function* allTargetOptions( - target: workspaces.TargetDefinition, - skipBaseOptions = false -): Iterable<[string | undefined, Record]> { - if (!skipBaseOptions && target.options) { - yield [undefined, target.options]; - } - - if (!target.configurations) { - return; - } - - for (const [name, options] of Object.entries(target.configurations)) { - if (options !== undefined) { - yield [name, options]; - } - } -}