|
| 1 | +/** |
| 2 | + * @license |
| 3 | + * Copyright Google LLC All Rights Reserved. |
| 4 | + * |
| 5 | + * Use of this source code is governed by an MIT-style license that can be |
| 6 | + * found in the LICENSE file at https://angular.dev/license |
| 7 | + */ |
| 8 | + |
| 9 | +import { type JsonObject, type JsonValue, isJsonObject } from '@angular-devkit/core'; |
| 10 | +import { |
| 11 | + type Rule, |
| 12 | + SchematicsException, |
| 13 | + type Tree, |
| 14 | + apply, |
| 15 | + applyTemplates, |
| 16 | + chain, |
| 17 | + mergeWith, |
| 18 | + move, |
| 19 | + noop, |
| 20 | + schematic, |
| 21 | + strings, |
| 22 | + url, |
| 23 | +} from '@angular-devkit/schematics'; |
| 24 | +import { basename, join, relative } from 'node:path/posix'; |
| 25 | +import { JSONFile } from '../utility/json-file'; |
| 26 | +import { createProjectSchematic } from '../utility/project'; |
| 27 | +import { ProjectType } from '../utility/workspace-models'; |
| 28 | +import type { Schema as EntryPointOptions } from './schema'; |
| 29 | + |
| 30 | +function updateTsConfig(packageName: string, ...paths: string[]): Rule { |
| 31 | + return (host: Tree) => { |
| 32 | + if (!host.exists('tsconfig.json')) { |
| 33 | + return host; |
| 34 | + } |
| 35 | + |
| 36 | + const file = new JSONFile(host, 'tsconfig.json'); |
| 37 | + const jsonPath = ['compilerOptions', 'paths', packageName]; |
| 38 | + const value = file.get(jsonPath); |
| 39 | + file.modify(jsonPath, Array.isArray(value) ? Array.from(new Set([...value, ...paths])) : paths); |
| 40 | + }; |
| 41 | +} |
| 42 | + |
| 43 | +export default createProjectSchematic<EntryPointOptions>((options, { project, tree }) => { |
| 44 | + if (project.extensions['projectType'] !== ProjectType.Library) { |
| 45 | + throw new SchematicsException(`Project "${options.project}" is not a library project.`); |
| 46 | + } |
| 47 | + |
| 48 | + if (options.name === 'lib' || options.name.startsWith('lib/')) { |
| 49 | + throw new SchematicsException( |
| 50 | + `Entry point name "${options.name}" cannot be "lib" or start with "lib/" ` + |
| 51 | + 'as it conflicts with the primary entry point source directory.', |
| 52 | + ); |
| 53 | + } |
| 54 | + |
| 55 | + const libDir = project.root; |
| 56 | + const packageJsonPath = join(libDir, 'package.json'); |
| 57 | + if (!tree.exists(packageJsonPath)) { |
| 58 | + throw new SchematicsException(`Could not find package.json at "${packageJsonPath}".`); |
| 59 | + } |
| 60 | + |
| 61 | + const packageJson = new JSONFile(tree, packageJsonPath); |
| 62 | + const rawPackageName = packageJson.get(['name']); |
| 63 | + const packageName = |
| 64 | + typeof rawPackageName === 'string' && rawPackageName |
| 65 | + ? rawPackageName |
| 66 | + : strings.dasherize(options.project); |
| 67 | + |
| 68 | + const isPrimary = options.name === '.'; |
| 69 | + const entryFile = (options.entryFile ?? 'public-api').replace(/\.[cm]?ts$/i, ''); |
| 70 | + const sourceRoot = project.sourceRoot ?? join(libDir, 'src'); |
| 71 | + const entryPointDir = join(sourceRoot, options.name); |
| 72 | + const exportKey = isPrimary ? '.' : `./${options.name}`; |
| 73 | + const exportTarget = `./${relative(libDir, join(entryPointDir, `${entryFile}.ts`))}`; |
| 74 | + |
| 75 | + const existingExports = (packageJson.get(['exports']) as JsonValue | undefined) ?? null; |
| 76 | + let exportsObject: JsonObject = {}; |
| 77 | + if (typeof existingExports === 'string' || Array.isArray(existingExports)) { |
| 78 | + exportsObject = { '.': existingExports }; |
| 79 | + } else if (isJsonObject(existingExports)) { |
| 80 | + const keys = Object.keys(existingExports); |
| 81 | + if (keys.some((k) => k.startsWith('.'))) { |
| 82 | + exportsObject = { ...existingExports }; |
| 83 | + } else if (keys.length > 0) { |
| 84 | + exportsObject = { '.': existingExports }; |
| 85 | + } |
| 86 | + } |
| 87 | + |
| 88 | + if (Object.prototype.hasOwnProperty.call(exportsObject, exportKey)) { |
| 89 | + throw new SchematicsException( |
| 90 | + `Entry point "${exportKey}" already exists in "${packageJsonPath}".`, |
| 91 | + ); |
| 92 | + } |
| 93 | + |
| 94 | + if ( |
| 95 | + isJsonObject(existingExports) && |
| 96 | + Object.keys(existingExports).some((k) => k.startsWith('.')) |
| 97 | + ) { |
| 98 | + packageJson.modify(['exports', exportKey], exportTarget); |
| 99 | + } else { |
| 100 | + packageJson.modify(['exports'], { |
| 101 | + ...exportsObject, |
| 102 | + [exportKey]: exportTarget, |
| 103 | + }); |
| 104 | + } |
| 105 | + |
| 106 | + const prefix = options.prefix ?? project.prefix ?? 'lib'; |
| 107 | + const sourceDir = join(entryPointDir, 'lib'); |
| 108 | + const artifactName = isPrimary |
| 109 | + ? /^@.*\/.*/.test(options.project) |
| 110 | + ? options.project.split('/')[1] |
| 111 | + : options.project |
| 112 | + : basename(options.name); |
| 113 | + |
| 114 | + const buildOptions = project.targets.get('build')?.options; |
| 115 | + let distRoot: string; |
| 116 | + if (typeof buildOptions?.['outputPath'] === 'string') { |
| 117 | + distRoot = buildOptions['outputPath']; |
| 118 | + } else { |
| 119 | + let folderName = options.project.startsWith('@') ? options.project.slice(1) : options.project; |
| 120 | + if (/[A-Z]/.test(folderName)) { |
| 121 | + folderName = strings.dasherize(folderName); |
| 122 | + } |
| 123 | + distRoot = `dist/${folderName}`; |
| 124 | + } |
| 125 | + |
| 126 | + const tsConfigPathKey = isPrimary ? packageName : `${packageName}/${options.name}`; |
| 127 | + const tsConfigPathTarget = `./${join(distRoot, options.name)}`; |
| 128 | + |
| 129 | + const templateSource = apply(url('./files'), [ |
| 130 | + applyTemplates({ |
| 131 | + ...strings, |
| 132 | + ...options, |
| 133 | + name: artifactName, |
| 134 | + entryFile, |
| 135 | + }), |
| 136 | + move(entryPointDir), |
| 137 | + ]); |
| 138 | + |
| 139 | + return chain([ |
| 140 | + mergeWith(templateSource), |
| 141 | + options.skipTsConfig ? noop() : updateTsConfig(tsConfigPathKey, tsConfigPathTarget), |
| 142 | + options.standalone |
| 143 | + ? noop() |
| 144 | + : schematic('module', { |
| 145 | + name: artifactName, |
| 146 | + commonModule: false, |
| 147 | + flat: true, |
| 148 | + path: sourceDir, |
| 149 | + project: options.project, |
| 150 | + // Explicitly set the `typeSeparator` this also ensures that the generated files are valid even if the `module` schematic |
| 151 | + // inherits its `typeSeparator` from the workspace. |
| 152 | + typeSeparator: '-', |
| 153 | + }), |
| 154 | + schematic('component', { |
| 155 | + name: artifactName, |
| 156 | + selector: `${prefix}-${strings.dasherize(artifactName)}`, |
| 157 | + inlineStyle: true, |
| 158 | + inlineTemplate: true, |
| 159 | + flat: true, |
| 160 | + path: sourceDir, |
| 161 | + export: true, |
| 162 | + standalone: options.standalone, |
| 163 | + skipTests: options.skipTests, |
| 164 | + project: options.project, |
| 165 | + // Explicitly set an empty `type` since it doesn't necessarily make sense in a library. |
| 166 | + // This also ensures that the generated files are valid even if the `component` schematic |
| 167 | + // inherits its `type` from the workspace. |
| 168 | + type: '', |
| 169 | + }), |
| 170 | + ]); |
| 171 | +}); |
0 commit comments