diff --git a/package-lock.json b/package-lock.json index c57ff0e..84666ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -48,15 +48,6 @@ } } }, - "@angular/cdk": { - "version": "8.2.3", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-8.2.3.tgz", - "integrity": "sha512-ZwO5Sn720RA2YvBqud0JAHkZXjmjxM0yNzCO8RVtRE9i8Gl26Wk0j0nQeJkVm4zwv2QO8MwbKUKGTMt8evsokA==", - "requires": { - "parse5": "^5.0.0", - "tslib": "^1.7.1" - } - }, "@babel/code-frame": { "version": "7.5.5", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.5.5.tgz", diff --git a/package.json b/package.json index b086f78..d14aecd 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,6 @@ "dependencies": { "@angular-devkit/core": "^8.3.8", "@angular-devkit/schematics": "^8.3.8", - "@angular/cdk": "^8.2.3", "@schematics/angular": "^8.3.8", "@schematics/update": "^0.803.8", "rxjs": "^6.4.0", diff --git a/src/cdk/index.ts b/src/cdk/index.ts new file mode 100644 index 0000000..f49d4b2 --- /dev/null +++ b/src/cdk/index.ts @@ -0,0 +1,8 @@ +export * from './ng-add/package-config'; +export * from './utils/ast'; +export * from './utils/build-component'; +export * from './utils/get-project'; +export * from './utils/html-head-element'; +export * from './utils/project-main-file'; +export * from './utils/project-targets'; +export * from './utils/version-agnostic-typescript'; diff --git a/src/cdk/ng-add/package-config.ts b/src/cdk/ng-add/package-config.ts new file mode 100644 index 0000000..b431921 --- /dev/null +++ b/src/cdk/ng-add/package-config.ts @@ -0,0 +1,40 @@ +/** + * @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'; + +/** + * Sorts the keys of the given object. + * @returns A new object instance with sorted keys + */ +function sortObjectByKeys(obj: object) { + return Object.keys(obj) + .sort() + .reduce((result, key) => (result[key] = obj[key]) && result, {}); +} + +/** Adds a package to the package.json in the given host tree. */ +export function addPackageToPackageJson(host: Tree, pkg: string, version: string): Tree { + if (host.exists('package.json')) { + const sourceText = host.read('package.json')!.toString('utf-8'); + const json = JSON.parse(sourceText); + + if (!json.dependencies) { + json.dependencies = {}; + } + + if (!json.dependencies[pkg]) { + json.dependencies[pkg] = version; + json.dependencies = sortObjectByKeys(json.dependencies); + } + + host.overwrite('package.json', JSON.stringify(json, null, 2)); + } + + return host; +} diff --git a/src/cdk/utils/ast.ts b/src/cdk/utils/ast.ts new file mode 100644 index 0000000..eeb4be5 --- /dev/null +++ b/src/cdk/utils/ast.ts @@ -0,0 +1,76 @@ +/** + * @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 { WorkspaceProject } from '@angular-devkit/core/src/experimental/workspace'; +import { SchematicsException, Tree } from '@angular-devkit/schematics'; +import { Schema as ComponentOptions } from '@schematics/angular/component/schema'; +import { addImportToModule } from '@schematics/angular/utility/ast-utils'; +import { InsertChange } from '@schematics/angular/utility/change'; +import { getWorkspace } from '@schematics/angular/utility/config'; +import { findModuleFromOptions as internalFindModule } from '@schematics/angular/utility/find-module'; +import { getAppModulePath } from '@schematics/angular/utility/ng-ast-utils'; +import { getProjectMainFile } from './project-main-file'; +import { ts, typescript } from './version-agnostic-typescript'; + +/** Reads file given path and returns TypeScript source file. */ +export function getSourceFile(host: Tree, path: string): typescript.SourceFile { + const buffer = host.read(path); + if (!buffer) { + throw new SchematicsException(`Could not find file for path: ${path}`); + } + return ts.createSourceFile(path, buffer.toString(), ts.ScriptTarget.Latest, true); +} + +/** Import and add module to root app module. */ +export function addModuleImportToRootModule(host: Tree, moduleName: string, src: string, project: WorkspaceProject) { + const modulePath = getAppModulePath(host, getProjectMainFile(project)); + addModuleImportToModule(host, modulePath, moduleName, src); +} + +/** + * Import and add module to specific module path. + * @param host the tree we are updating + * @param modulePath src location of the module to import + * @param moduleName name of module to import + * @param src src location to import + */ +export function addModuleImportToModule(host: Tree, modulePath: string, moduleName: string, src: string) { + const moduleSource: any = getSourceFile(host, modulePath); + + if (!moduleSource) { + throw new SchematicsException(`Module not found: ${modulePath}`); + } + + const changes = addImportToModule(moduleSource, modulePath, moduleName, src); + const recorder = host.beginUpdate(modulePath); + + changes.forEach(change => { + if (change instanceof InsertChange) { + recorder.insertLeft(change.pos, change.toAdd); + } + }); + + host.commitUpdate(recorder); +} + +/** Wraps the internal find module from options with undefined path handling */ +export function findModuleFromOptions(host: Tree, options: ComponentOptions): string | undefined { + const workspace = getWorkspace(host); + + if (!options.project) { + options.project = Object.keys(workspace.projects)[0]; + } + + const project = workspace.projects[options.project]; + + if (options.path === undefined) { + options.path = `/${project.root}/src/app`; + } + + return internalFindModule(host, options); +} diff --git a/src/cdk/utils/ast/ng-module-imports.ts b/src/cdk/utils/ast/ng-module-imports.ts new file mode 100644 index 0000000..1c2266b --- /dev/null +++ b/src/cdk/utils/ast/ng-module-imports.ts @@ -0,0 +1,89 @@ +/** + * @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'; + +/** + * Whether the Angular module in the given path imports the specified module class name. + */ +export function hasNgModuleImport(tree: Tree, modulePath: string, className: string): boolean { + const moduleFileContent = tree.read(modulePath); + + if (!moduleFileContent) { + throw new SchematicsException(`Could not read Angular module file: ${modulePath}`); + } + + const parsedFile = ts.createSourceFile(modulePath, moduleFileContent.toString(), ts.ScriptTarget.Latest, true); + const ngModuleMetadata = findNgModuleMetadata(parsedFile); + + if (!ngModuleMetadata) { + throw new SchematicsException(`Could not find NgModule declaration inside: "${modulePath}"`); + } + + for (let property of ngModuleMetadata!.properties) { + if ( + !ts.isPropertyAssignment(property) || + property.name.getText() !== 'imports' || + !ts.isArrayLiteralExpression(property.initializer) + ) { + continue; + } + + if (property.initializer.elements.some(element => element.getText() === className)) { + return true; + } + } + + return false; +} + +/** + * Resolves the last identifier that is part of the given expression. This helps resolving + * identifiers of nested property access expressions (e.g. myNamespace.core.NgModule). + */ +function resolveIdentifierOfExpression(expression: ts.Expression): ts.Identifier | null { + if (ts.isIdentifier(expression)) { + return expression; + } else if (ts.isPropertyAccessExpression(expression)) { + return expression.name; + } + return null; +} + +/** + * Finds a NgModule declaration within the specified TypeScript node and returns the + * corresponding metadata for it. This function searches breadth first because + * NgModule's are usually not nested within other expressions or declarations. + */ +function findNgModuleMetadata(rootNode: ts.Node): ts.ObjectLiteralExpression | null { + // Add immediate child nodes of the root node to the queue. + const nodeQueue: ts.Node[] = [...rootNode.getChildren()]; + + while (nodeQueue.length) { + const node = nodeQueue.shift()!; + + if (ts.isDecorator(node) && ts.isCallExpression(node.expression) && isNgModuleCallExpression(node.expression)) { + return node.expression.arguments[0] as ts.ObjectLiteralExpression; + } else { + nodeQueue.push(...node.getChildren()); + } + } + + return null; +} + +/** Whether the specified call expression is referring to a NgModule definition. */ +function isNgModuleCallExpression(callExpression: ts.CallExpression): boolean { + if (!callExpression.arguments.length || !ts.isObjectLiteralExpression(callExpression.arguments[0])) { + return false; + } + + const decoratorIdentifier = resolveIdentifierOfExpression(callExpression.expression); + return decoratorIdentifier ? decoratorIdentifier.text === 'NgModule' : false; +} diff --git a/src/cdk/utils/build-component.ts b/src/cdk/utils/build-component.ts new file mode 100644 index 0000000..e692e0a --- /dev/null +++ b/src/cdk/utils/build-component.ts @@ -0,0 +1,246 @@ +/** + * @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 { strings, template as interpolateTemplate } from '@angular-devkit/core'; +import { + apply, + applyTemplates, + branchAndMerge, + chain, + filter, + mergeWith, + move, + noop, + Rule, + SchematicsException, + Tree, + url +} from '@angular-devkit/schematics'; +import { FileSystemSchematicContext } from '@angular-devkit/schematics/tools'; +import { Schema as ComponentOptions, Style } from '@schematics/angular/component/schema'; +import { + addDeclarationToModule, + addEntryComponentToModule, + addExportToModule +} from '@schematics/angular/utility/ast-utils'; +import { InsertChange } from '@schematics/angular/utility/change'; +import { getWorkspace } from '@schematics/angular/utility/config'; +import { buildRelativePath, findModuleFromOptions } from '@schematics/angular/utility/find-module'; +import { parseName } from '@schematics/angular/utility/parse-name'; +import { buildDefaultPath } from '@schematics/angular/utility/project'; +import { validateHtmlSelector, validateName } from '@schematics/angular/utility/validation'; +import { readFileSync, statSync } from 'fs'; +import { dirname, join, resolve } from 'path'; +import { getProjectFromWorkspace } from './get-project'; +import { getDefaultComponentOptions } from './schematic-options'; +import { ts } from './version-agnostic-typescript'; + +/** + * List of style extensions which are CSS compatible. All supported CLI style extensions can be + * found here: angular/angular-cli/master/packages/schematics/angular/ng-new/schema.json#L118-L122 + */ +const supportedCssExtensions = ['css', 'scss', 'less']; + +function readIntoSourceFile(host: Tree, modulePath: string) { + const text = host.read(modulePath); + if (text === null) { + throw new SchematicsException(`File ${modulePath} does not exist.`); + } + + return ts.createSourceFile(modulePath, text.toString('utf-8'), ts.ScriptTarget.Latest, true); +} + +function addDeclarationToNgModule(options: ComponentOptions): Rule { + return (host: Tree) => { + if (options.skipImport || !options.module) { + return host; + } + + const modulePath = options.module; + let source: any = readIntoSourceFile(host, modulePath); + + const componentPath = + `/${options.path}/` + + (options.flat ? '' : strings.dasherize(options.name) + '/') + + strings.dasherize(options.name) + + '.component'; + const relativePath = buildRelativePath(modulePath, componentPath); + const classifiedName = strings.classify(`${options.name}Component`); + + const declarationChanges = addDeclarationToModule(source, modulePath, classifiedName, relativePath); + + const declarationRecorder = host.beginUpdate(modulePath); + for (const change of declarationChanges) { + if (change instanceof InsertChange) { + declarationRecorder.insertLeft(change.pos, change.toAdd); + } + } + host.commitUpdate(declarationRecorder); + + if (options.export) { + // Need to refresh the AST because we overwrote the file in the host. + source = readIntoSourceFile(host, modulePath); + + const exportRecorder = host.beginUpdate(modulePath); + const exportChanges = addExportToModule( + source, + modulePath, + strings.classify(`${options.name}Component`), + relativePath + ); + + for (const change of exportChanges) { + if (change instanceof InsertChange) { + exportRecorder.insertLeft(change.pos, change.toAdd); + } + } + host.commitUpdate(exportRecorder); + } + + if (options.entryComponent) { + // Need to refresh the AST because we overwrote the file in the host. + source = readIntoSourceFile(host, modulePath); + + const entryComponentRecorder = host.beginUpdate(modulePath); + const entryComponentChanges = addEntryComponentToModule( + source, + modulePath, + strings.classify(`${options.name}Component`), + relativePath + ); + + for (const change of entryComponentChanges) { + if (change instanceof InsertChange) { + entryComponentRecorder.insertLeft(change.pos, change.toAdd); + } + } + host.commitUpdate(entryComponentRecorder); + } + + return host; + }; +} + +function buildSelector(options: ComponentOptions, projectPrefix: string) { + let selector = strings.dasherize(options.name); + if (options.prefix) { + selector = `${options.prefix}-${selector}`; + } else if (options.prefix === undefined && projectPrefix) { + selector = `${projectPrefix}-${selector}`; + } + + return selector; +} + +/** + * Indents the text content with the amount of specified spaces. The spaces will be added after + * every line-break. This utility function can be used inside of EJS templates to properly + * include the additional files. + */ +function indentTextContent(text: string, numSpaces: number): string { + // In the Material project there should be only LF line-endings, but the schematic files + // are not being linted and therefore there can be also CRLF or just CR line-endings. + return text.replace(/(\r\n|\r|\n)/g, `$1${' '.repeat(numSpaces)}`); +} + +/** + * Rule that copies and interpolates the files that belong to this schematic context. Additionally + * a list of file paths can be passed to this rule in order to expose them inside the EJS + * template context. + * + * This allows inlining the external template or stylesheet files in EJS without having + * to manually duplicate the file content. + */ +export function buildComponent(options: ComponentOptions, additionalFiles: { [key: string]: string } = {}): Rule { + return (host: Tree, context: FileSystemSchematicContext) => { + const workspace = getWorkspace(host); + const project = getProjectFromWorkspace(workspace, options.project); + const defaultComponentOptions = getDefaultComponentOptions(project); + + // TODO(devversion): Remove if we drop support for older CLI versions. + // This handles an unreported breaking change from the @angular-devkit/schematics. Previously + // the description path resolved to the factory file, but starting from 6.2.0, it resolves + // to the factory directory. + const schematicPath = statSync(context.schematic.description.path).isDirectory() + ? context.schematic.description.path + : dirname(context.schematic.description.path); + + const schematicFilesUrl = './files'; + const schematicFilesPath = resolve(schematicPath, schematicFilesUrl); + + // Add the default component option values to the options if an option is not explicitly + // specified but a default component option is available. + Object.keys(options) + .filter(optionName => options[optionName] == null && defaultComponentOptions[optionName]) + .forEach(optionName => (options[optionName] = defaultComponentOptions[optionName])); + + if (options.path === undefined) { + // TODO(jelbourn): figure out if the need for this `as any` is a bug due to two different + // incompatible `WorkspaceProject` classes in @angular-devkit + options.path = buildDefaultPath(project as any); + } + + options.module = findModuleFromOptions(host, options); + + const parsedPath = parseName(options.path!, options.name); + + options.name = parsedPath.name; + options.path = parsedPath.path; + options.selector = options.selector || buildSelector(options, project.prefix); + + validateName(options.name); + validateHtmlSelector(options.selector!); + + // In case the specified style extension is not part of the supported CSS supersets, + // we generate the stylesheets with the "css" extension. This ensures that we don't + // accidentally generate invalid stylesheets (e.g. drag-drop-comp.styl) which will + // break the Angular CLI project. See: https://github.com/angular/components/issues/15164 + if (!supportedCssExtensions.includes(options.style!)) { + // TODO: Cast is necessary as we can't use the Style enum which has been introduced + // within CLI v7.3.0-rc.0. This would break the schematic for older CLI versions. + options.style = 'css' as Style; + } + + // Object that will be used as context for the EJS templates. + const baseTemplateContext = { + ...strings, + 'if-flat': (s: string) => (options.flat ? '' : s), + ...options + }; + + // Key-value object that includes the specified additional files with their loaded content. + // The resolved contents can be used inside EJS templates. + const resolvedFiles = {}; + + for (let key in additionalFiles) { + if (additionalFiles[key]) { + const fileContent = readFileSync(join(schematicFilesPath, additionalFiles[key]), 'utf-8'); + + // Interpolate the additional files with the base EJS template context. + resolvedFiles[key] = interpolateTemplate(fileContent)(baseTemplateContext); + } + } + + const templateSource = apply(url(schematicFilesUrl), [ + options.skipTests ? filter(path => !path.endsWith('.spec.ts.template')) : noop(), + options.inlineStyle ? filter(path => !path.endsWith('.__style__.template')) : noop(), + options.inlineTemplate ? filter(path => !path.endsWith('.html.template')) : noop(), + // Treat the template options as any, because the type definition for the template options + // is made unnecessarily explicit. Every type of object can be used in the EJS template. + applyTemplates({ indentTextContent, resolvedFiles, ...baseTemplateContext } as any), + // TODO(devversion): figure out why we cannot just remove the first parameter + // See for example: angular-cli#schematics/angular/component/index.ts#L160 + move(null as any, parsedPath.path) + ]); + + return chain([branchAndMerge(chain([addDeclarationToNgModule(options), mergeWith(templateSource)]))])( + host, + context + ); + }; +} diff --git a/src/cdk/utils/get-project.ts b/src/cdk/utils/get-project.ts new file mode 100644 index 0000000..cad92e1 --- /dev/null +++ b/src/cdk/utils/get-project.ts @@ -0,0 +1,24 @@ +/** + * @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 { WorkspaceSchema, WorkspaceProject } from '@angular-devkit/core/src/experimental/workspace'; +import { SchematicsException } from '@angular-devkit/schematics'; + +/** + * Finds the specified project configuration in the workspace. Throws an error if the project + * couldn't be found. + */ +export function getProjectFromWorkspace(workspace: WorkspaceSchema, projectName?: string): WorkspaceProject { + const project = workspace.projects[projectName || workspace.defaultProject!]; + + if (!project) { + throw new SchematicsException(`Could not find project in workspace: ${projectName}`); + } + + return project; +} diff --git a/src/cdk/utils/html-head-element.ts b/src/cdk/utils/html-head-element.ts new file mode 100644 index 0000000..3fba20b --- /dev/null +++ b/src/cdk/utils/html-head-element.ts @@ -0,0 +1,60 @@ +/** + * @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 { getChildElementIndentation } from './parse5-element'; +import { DefaultTreeDocument, DefaultTreeElement, parse as parseHtml } from 'parse5'; + +/** Appends the given element HTML fragment to the `` element of the specified HTML file. */ +export function appendHtmlElementToHead(host: Tree, htmlFilePath: string, elementHtml: string) { + const htmlFileBuffer = host.read(htmlFilePath); + + if (!htmlFileBuffer) { + throw new SchematicsException(`Could not read file for path: ${htmlFilePath}`); + } + + const htmlContent = htmlFileBuffer.toString(); + + if (htmlContent.includes(elementHtml)) { + return; + } + + const headTag = getHtmlHeadTagElement(htmlContent); + + if (!headTag) { + throw `Could not find '' element in HTML file: ${htmlFileBuffer}`; + } + + // We always have access to the source code location here because the `getHeadTagElement` + // function explicitly has the `sourceCodeLocationInfo` option enabled. + const endTagOffset = headTag.sourceCodeLocation!.endTag.startOffset; + const indentationOffset = getChildElementIndentation(headTag); + const insertion = `${' '.repeat(indentationOffset)}${elementHtml}`; + + const recordedChange = host.beginUpdate(htmlFilePath).insertRight(endTagOffset, `${insertion}\n`); + + host.commitUpdate(recordedChange); +} + +/** Parses the given HTML file and returns the head element if available. */ +export function getHtmlHeadTagElement(htmlContent: string): DefaultTreeElement | null { + const document = parseHtml(htmlContent, { sourceCodeLocationInfo: true }) as DefaultTreeDocument; + const nodeQueue = [...document.childNodes]; + + while (nodeQueue.length) { + const node = nodeQueue.shift() as DefaultTreeElement; + + if (node.nodeName.toLowerCase() === 'head') { + return node; + } else if (node.childNodes) { + nodeQueue.push(...node.childNodes); + } + } + + return null; +} diff --git a/src/cdk/utils/parse5-element.ts b/src/cdk/utils/parse5-element.ts new file mode 100644 index 0000000..ad49b69 --- /dev/null +++ b/src/cdk/utils/parse5-element.ts @@ -0,0 +1,34 @@ +/** + * @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'; +import { DefaultTreeElement } from 'parse5'; + +/** Determines the indentation of child elements for the given Parse5 element. */ +export function getChildElementIndentation(element: DefaultTreeElement) { + const childElement = element.childNodes.find(node => node['tagName']) as DefaultTreeElement | null; + + if ((childElement && !childElement.sourceCodeLocation) || !element.sourceCodeLocation) { + throw new SchematicsException( + 'Cannot determine child element indentation because the ' + + 'specified Parse5 element does not have any source code location metadata.' + ); + } + + const startColumns = childElement + ? // In case there are child elements inside of the element, we assume that their + // indentation is also applicable for other child elements. + childElement.sourceCodeLocation!.startCol + : // In case there is no child element, we just assume that child elements should be indented + // by two spaces. + element.sourceCodeLocation!.startCol + 2; + + // Since Parse5 does not set the `startCol` properties as zero-based, we need to subtract + // one column in order to have a proper zero-based offset for the indentation. + return startColumns - 1; +} diff --git a/src/cdk/utils/project-main-file.ts b/src/cdk/utils/project-main-file.ts new file mode 100644 index 0000000..f61268d --- /dev/null +++ b/src/cdk/utils/project-main-file.ts @@ -0,0 +1,24 @@ +/** + * @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 { WorkspaceProject } from '@angular-devkit/core/src/experimental/workspace'; +import { SchematicsException } from '@angular-devkit/schematics'; +import { getProjectTargetOptions } from './project-targets'; + +/** Looks for the main TypeScript file in the given project and returns its path. */ +export function getProjectMainFile(project: WorkspaceProject): string { + const buildOptions = getProjectTargetOptions(project, 'build'); + + if (!buildOptions.main) { + throw new SchematicsException( + `Could not find the project main file inside of the ` + `workspace config (${project.sourceRoot})` + ); + } + + return buildOptions.main; +} diff --git a/src/cdk/utils/project-targets.ts b/src/cdk/utils/project-targets.ts new file mode 100644 index 0000000..4e0e473 --- /dev/null +++ b/src/cdk/utils/project-targets.ts @@ -0,0 +1,26 @@ +/** + * @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 { WorkspaceProject } from '@angular-devkit/core/src/experimental/workspace'; +import { SchematicsException } from '@angular-devkit/schematics'; + +/** Resolves the architect options for the build target of the given project. */ +export function getProjectTargetOptions(project: WorkspaceProject, buildTarget: string) { + if (project.targets && project.targets[buildTarget] && project.targets[buildTarget].options) { + return project.targets[buildTarget].options; + } + + // TODO(devversion): consider removing this architect check if the CLI completely switched + // over to `targets`, and the `architect` support has been removed. + // See: https://github.com/angular/angular-cli/commit/307160806cb48c95ecb8982854f452303801ac9f + if (project.architect && project.architect[buildTarget] && project.architect[buildTarget].options) { + return project.architect[buildTarget].options; + } + + throw new SchematicsException(`Cannot determine project target configuration for: ${buildTarget}.`); +} diff --git a/src/cdk/utils/schematic-options.ts b/src/cdk/utils/schematic-options.ts new file mode 100644 index 0000000..f7ef040 --- /dev/null +++ b/src/cdk/utils/schematic-options.ts @@ -0,0 +1,56 @@ +/** + * @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 { WorkspaceProject } from '@angular-devkit/core/src/experimental/workspace'; + +/** + * Returns the default options for the `@schematics/angular:component` schematic which would + * have been specified at project initialization (ng new or ng init). + * + * This is necessary because the Angular CLI only exposes the default values for the "--style", + * "--inlineStyle", "--skipTests" and "--inlineTemplate" options to the "component" schematic. + */ +export function getDefaultComponentOptions(project: WorkspaceProject) { + // Note: Not all options which are available when running "ng new" will be stored in the + // workspace config. List of options which will be available in the configuration: + // angular/angular-cli/blob/master/packages/schematics/angular/application/index.ts#L109-L131 + let skipTests = getDefaultComponentOption(project, ['skipTests'], null); + + // In case "skipTests" is not set explicitly, also look for the "spec" option. The "spec" + // option has been deprecated but can be still used in older Angular CLI projects. + // See: https://github.com/angular/angular-cli/commit/a12a4e02a4689b5bdbc6e740c0d9865afb55671a + if (skipTests === null) { + skipTests = !getDefaultComponentOption(project, ['spec'], true); + } + + return { + style: getDefaultComponentOption(project, ['style', 'styleext'], 'css'), + inlineStyle: getDefaultComponentOption(project, ['inlineStyle'], false), + inlineTemplate: getDefaultComponentOption(project, ['inlineTemplate'], false), + skipTests: skipTests + }; +} + +/** + * Gets the default value for the specified option. The default options will be determined + * by looking at the stored schematic options for `@schematics/angular:component` in the + * CLI workspace configuration. + */ +function getDefaultComponentOption(project: WorkspaceProject, optionNames: string[], fallbackValue: T): T { + for (let optionName of optionNames) { + if ( + project.schematics && + project.schematics['@schematics/angular:component'] && + project.schematics['@schematics/angular:component'][optionName] != null + ) { + return project.schematics['@schematics/angular:component'][optionName]; + } + } + + return fallbackValue; +} diff --git a/src/cdk/utils/version-agnostic-typescript.ts b/src/cdk/utils/version-agnostic-typescript.ts new file mode 100644 index 0000000..f22fd5b --- /dev/null +++ b/src/cdk/utils/version-agnostic-typescript.ts @@ -0,0 +1,48 @@ +/** + * @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 + */ + +/** + * This is just a type import and won't be generated in the release output. + * + * Note that we always need to adjust this type import based on the location of the Typescript + * dependency that will be shipped with `@schematics/angular`. + */ +import typescript = require('typescript'); +import { SchematicsException } from '@angular-devkit/schematics'; + +/** + * This is an agnostic re-export of TypeScript. Depending on the context, this module file will + * return the TypeScript version that is being shipped within the `@schematics/angular` package, + * or fall back to the TypeScript version that has been flattened in the node modules. + * + * This is necessary because we parse TypeScript files and pass the resolved AST to the + * `@schematics/angular` package which might have a different TypeScript version installed. + */ +let ts: typeof typescript; + +try { + ts = require('@schematics/angular/third_party/github.com/Microsoft/TypeScript/lib/typescript'); +} catch { + // Fallback for CLI versions before v8.0.0. The TypeScript dependency has been dropped in + // CLI version v8.0.0 but older CLI versions can still run the latest generation schematics. + // See: https://github.com/angular/angular-cli/commit/bf1c069f73c8e3d4f0e8d584cbfb47c408c1730b + try { + ts = require('@schematics/angular/node_modules/typescript'); + } catch { + try { + ts = require('typescript'); + } catch { + throw new SchematicsException( + 'Error: Could not find a TypeScript version for the ' + + 'schematics. Please report an issue on the Angular Material repository.' + ); + } + } +} + +export { ts, typescript }; diff --git a/src/material/ast.ts b/src/material/ast.ts index 2e3c8aa..4258f69 100644 --- a/src/material/ast.ts +++ b/src/material/ast.ts @@ -1,11 +1,11 @@ import { Tree } from '@angular-devkit/schematics'; +import { WorkspaceProject } from '../angular'; import { - getSourceFile as originalGetSourceFile, - addModuleImportToRootModule as originalAddModuleImportToRootModule, addModuleImportToModule as originalAddModuleImportToModule, + addModuleImportToRootModule as originalAddModuleImportToRootModule, + getSourceFile as originalGetSourceFile, typescript -} from '@angular/cdk/schematics'; -import { WorkspaceProject } from '@angular-devkit/core/src/experimental/workspace'; +} from '../cdk'; /** Reads file given path and returns TypeScript source file. */ export function getSourceFile(host: Tree, path: string): typescript.SourceFile { diff --git a/src/material/ast/ng-module-imports.ts b/src/material/ast/ng-module-imports.ts index 154aa42..a86ef70 100644 --- a/src/material/ast/ng-module-imports.ts +++ b/src/material/ast/ng-module-imports.ts @@ -1,5 +1,5 @@ import { Tree } from '@angular-devkit/schematics'; -import { hasNgModuleImport as originalHasNgModuleImport } from '@angular/cdk/schematics'; +import { hasNgModuleImport as originalHasNgModuleImport } from '../ast/ng-module-imports'; /** * Whether the Angular module in the given path imports the specified module class name. diff --git a/src/material/build-component.ts b/src/material/build-component.ts index f0cc62a..689152d 100644 --- a/src/material/build-component.ts +++ b/src/material/build-component.ts @@ -1,6 +1,6 @@ import { Rule } from '@angular-devkit/schematics'; -import { Schema as ComponentOptions } from '@schematics/angular/component/schema'; -import { buildComponent as originalBuildComponent } from '@angular/cdk/schematics'; +import { Schema as ComponentOptions } from '../angular'; +import { buildComponent as originalBuildComponent } from '../cdk'; /** * Rule that copies and interpolates the files that belong to this schematic context. Additionally @@ -11,5 +11,5 @@ import { buildComponent as originalBuildComponent } from '@angular/cdk/schematic * to manually duplicate the file content. */ export function buildComponent(options: ComponentOptions, additionalFiles: { [key: string]: string } = {}): Rule { - return originalBuildComponent(options, additionalFiles); + return originalBuildComponent(options, additionalFiles); } diff --git a/src/material/get-project.ts b/src/material/get-project.ts index d69bb0b..c59d06c 100644 --- a/src/material/get-project.ts +++ b/src/material/get-project.ts @@ -1,10 +1,10 @@ -import { getProjectFromWorkspace as originalGetProjectFromWorkspace } from '@angular/cdk/schematics'; -import { WorkspaceSchema, WorkspaceProject } from '@angular-devkit/core/src/experimental/workspace'; +import { WorkspaceProject, WorkspaceSchema } from '../angular'; +import { getProjectFromWorkspace as originalGetProjectFromWorkspace } from '../cdk'; /** * Finds the specified project configuration in the workspace. Throws an error if the project * couldn't be found. */ -export function getProjectFromWorkspace(workspace: WorkspaceSchema, projectName?: string): WorkspaceProject { +export function getProjectFromWorkspace(workspace: WorkspaceSchema, projectName?: string) { return originalGetProjectFromWorkspace(workspace, projectName); } diff --git a/src/material/html-head-element.ts b/src/material/html-head-element.ts index 3fedc00..f2e9894 100644 --- a/src/material/html-head-element.ts +++ b/src/material/html-head-element.ts @@ -1,9 +1,9 @@ import { Tree } from '@angular-devkit/schematics'; +import { DefaultTreeElement } from 'parse5'; import { appendHtmlElementToHead as originalAppendHtmlElementToHead, getHtmlHeadTagElement as originalGetHtmlHeadTagElement -} from '@angular/cdk/schematics'; -import { DefaultTreeElement } from 'parse5'; +} from '../cdk'; /** Appends the given element HTML fragment to the `` element of the specified HTML file. */ export function appendHtmlElementToHead(host: Tree, htmlFilePath: string, elementHtml: string) { diff --git a/src/material/package-config.ts b/src/material/package-config.ts index d51a930..47ad2bd 100644 --- a/src/material/package-config.ts +++ b/src/material/package-config.ts @@ -1,5 +1,5 @@ import { Tree } from '@angular-devkit/schematics'; -import { addPackageToPackageJson as originalAddPackageToPackageJson } from '@angular/cdk/schematics/ng-add/package-config'; +import { addPackageToPackageJson as originalAddPackageToPackageJson } from '../cdk'; /** Adds a package to the package.json in the given host tree. */ export function addPackageToPackageJson(host: Tree, pkg: string, version: string): Tree {