├── .gitignore ├── .DS_Store ├── src ├── tasks │ ├── index.ts │ ├── bump.json │ ├── node-lib.json │ ├── ngc-cli-utils.ts │ ├── copy-file.json │ ├── bump.ts │ ├── copy-file.ts │ └── node-lib.ts ├── index.ts └── build │ ├── job.ts │ ├── schema.json │ ├── hook-registry.ts │ ├── index.ts │ ├── create-hook-provider.ts │ ├── hooks.ts │ └── utils.ts ├── .npmignore ├── examples ├── filter-packages │ ├── README.md │ └── filter-packages.ts ├── node-library │ ├── node-library.ts │ ├── tsconfig.node-lib.json │ └── README.md ├── copy-files-and-bump │ ├── copy-files-and-bump.ts │ └── README.md ├── api-generator │ ├── README.md │ └── api-generator.ts └── update-tsconfig-for-secondary-entry-points │ ├── README.md │ └── update-tsconfig-for-secondary-entry-points.ts ├── publish-copy.mjs ├── tsconfig.json ├── tsconfig.editor.json ├── LICENSE ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | .vscode 4 | .DS_Store -------------------------------------------------------------------------------- /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/shlomiassaf/ng-cli-packagr-tasks/HEAD/.DS_Store -------------------------------------------------------------------------------- /src/tasks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './copy-file'; 2 | export * from './node-lib'; 3 | export * from './bump'; -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | tsconfig.json 2 | examples 3 | src/**/*.ts 4 | publish-copy.js 5 | .vscode 6 | node_modules 7 | yarn.lock 8 | .DS_Store -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright Google Inc. 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.io/license 7 | */ 8 | 9 | export * from './build'; 10 | -------------------------------------------------------------------------------- /examples/filter-packages/README.md: -------------------------------------------------------------------------------- 1 | # Example: Filter all secondary endpoints, build only primary 2 | 3 | This example does not use a built-in task, instead it will provide a handler to the 4 | `initTsConfig` hook at the `before` phase and will just remove all secondary entry points from the primary entry point. 5 | 6 | This is a simple task that demonstrate the use of `ng-packagr` API. 7 | -------------------------------------------------------------------------------- /examples/node-library/node-library.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Example: Copy files and bump version 3 | */ 4 | 5 | import { NgPackagerHooksContext, HookRegistry } from 'ng-cli-packagr-tasks'; 6 | import { NodeLib } from 'ng-cli-packagr-tasks/dist/tasks/node-lib'; 7 | 8 | module.exports = function(ctx: NgPackagerHooksContext, registry: HookRegistry) { 9 | registry 10 | .register(NodeLib); 11 | } 12 | -------------------------------------------------------------------------------- /src/tasks/bump.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Webpack browser schema for Build Facade.", 3 | "description": "Browser target options", 4 | "properties": { 5 | "bump": { 6 | "type": "string", 7 | "description": "What to bump (semver)", 8 | "enum": [ 9 | "major", 10 | "premajor", 11 | "minor", 12 | "preminor", 13 | "patch", 14 | "prepatch", 15 | "prerelease" 16 | ] 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /examples/node-library/tsconfig.node-lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "declaration": true, 7 | "sourceMap": true, 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "importHelpers": true, 11 | "lib": [ 12 | "dom", 13 | "es2018" 14 | ] 15 | }, 16 | "exclude": [ 17 | "src/test.ts", 18 | "**/*.spec.ts" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /publish-copy.mjs: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as fs from 'fs'; 3 | import * as globby from 'globby'; 4 | 5 | const cwd = process.cwd(); 6 | const srcRoot = path.join(cwd, 'src'); 7 | const destRoot = path.join(cwd, 'dist'); 8 | 9 | const toCopy = globby.sync('**/*.json', { cwd: srcRoot }) 10 | .map( p => ({ 11 | from: path.join(srcRoot, p), 12 | to: path.join(destRoot, p), 13 | })); 14 | 15 | for (const ci of toCopy) { 16 | fs.copyFileSync(ci.from, ci.to); 17 | } -------------------------------------------------------------------------------- /examples/copy-files-and-bump/copy-files-and-bump.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Example: Copy files and bump version 3 | */ 4 | 5 | import { NgPackagerHooks, NgPackagerHooksContext, HookRegistry } from 'ng-cli-packagr-tasks'; 6 | import { CopyFile } from 'ng-cli-packagr-tasks/dist/tasks/copy-file'; 7 | import { Bump } from 'ng-cli-packagr-tasks/dist/tasks/bump'; 8 | 9 | module.exports = function(ctx: NgPackagerHooksContext, registry: HookRegistry) { 10 | registry 11 | .register(CopyFile) 12 | .register(Bump); 13 | } 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "rootDir": "src", 6 | "outDir": "dist", 7 | "sourceMap": true, 8 | "declaration": true, 9 | "declarationMap": true, 10 | "moduleResolution": "node", 11 | "module": "commonjs", 12 | "emitDecoratorMetadata": true, 13 | "experimentalDecorators": true, 14 | "target": "es6", 15 | "lib": [ 16 | "ES6" 17 | ], 18 | "skipLibCheck": true 19 | }, 20 | "exclude": [ 21 | "dist", 22 | "examples", 23 | "node_modules", 24 | "tmp" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /tsconfig.editor.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "rootDir": "src", 6 | "outDir": "dist", 7 | "sourceMap": true, 8 | "declaration": true, 9 | "declarationMap": true, 10 | "moduleResolution": "node", 11 | "module": "commonjs", 12 | "emitDecoratorMetadata": true, 13 | "experimentalDecorators": true, 14 | "target": "es6", 15 | "lib": [ 16 | "ES6" 17 | ], 18 | "skipLibCheck": true 19 | }, 20 | "exclude": [ 21 | "dist", 22 | "examples", 23 | "node_modules", 24 | "tmp" 25 | ] 26 | } 27 | -------------------------------------------------------------------------------- /src/build/job.ts: -------------------------------------------------------------------------------- 1 | import { NgPackagerHooks } from './hooks'; 2 | 3 | const store = new WeakMap(); 4 | 5 | export interface Type extends Function { 6 | new (...args: any[]): T 7 | } 8 | 9 | export interface JobMetadata { 10 | schema: string; 11 | selector: string; 12 | hooks: NgPackagerHooks; 13 | internalWatch?: boolean; 14 | } 15 | 16 | export function findJobMetadata(type: Type): JobMetadata | undefined { 17 | return store.get(type); 18 | } 19 | 20 | export function Job(metadata: JobMetadata) { 21 | return target => { 22 | store.set(target, metadata); 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /src/tasks/node-lib.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Webpack browser schema for Build Facade.", 3 | "description": "Browser target options", 4 | "properties": { 5 | "nodeLib": { 6 | "type": "object", 7 | "description": "Node library build configuration", 8 | "properties": { 9 | "tsConfig": { 10 | "type": "string", 11 | "description": "The file path of the TypeScript configuration file, specific for node library generation.\n\n If not set will use the tsConfig of the project" 12 | }, 13 | "compilerOptions": { 14 | "type": "object", 15 | "additionalProperties": true 16 | } 17 | } 18 | } 19 | } 20 | } -------------------------------------------------------------------------------- /examples/api-generator/README.md: -------------------------------------------------------------------------------- 1 | # Example: Create documentation after building package 2 | 3 | In this example we generate documentation for the library after it was built. 4 | This example doesn't render documentation, it will only create a JSON schema with all metadata required to build documentation for the library. 5 | To extract TS type metadata we are using `@microsoft/api-extractor`. 6 | 7 | This example does not use a built-in task, instead it will provide a handler to the 8 | `writePackage` hook at the `after` phase and will just remove all secondary entry points from the primary entry point. 9 | 10 | This is a simple task that demonstrate how we can chain operations. It also uses the `ng-packagr` API to find the 11 | file required to generate the docs. 12 | -------------------------------------------------------------------------------- /examples/filter-packages/filter-packages.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Example: Filter all secondary endpoints, build only primary. 3 | * 4 | * This can be extended to support compilation of affected packages only. (commit changed). 5 | */ 6 | 7 | 8 | import { NgPackagerHooks, NgPackagerHooksContext } from 'ng-cli-packagr-tasks'; 9 | import { isEntryPoint } from 'ng-packagr/lib/ng-v5/nodes'; 10 | 11 | module.exports = function(ctx: NgPackagerHooksContext) { 12 | const hooks: NgPackagerHooks = { 13 | initTsConfig: { 14 | before: async taskContext => { 15 | for (const entry of taskContext.graph.entries()) { 16 | if (isEntryPoint(entry)) { 17 | if (entry.data.entryPoint.isSecondaryEntryPoint) { 18 | entry.state = 'done'; 19 | } 20 | } 21 | } 22 | } 23 | }, 24 | }; 25 | return hooks; 26 | } 27 | -------------------------------------------------------------------------------- /examples/update-tsconfig-for-secondary-entry-points/README.md: -------------------------------------------------------------------------------- 1 | # Example: Update tsconfig settings before compilation in secondary entry points 2 | 3 | This example does not use a built-in task, instead it will provide a handler to the 4 | `initTsConfig` hook at the `after` phase and will update the typescript configuration of each secondary build. 5 | 6 | This is a simple way to differentiate TS compilation for secondary entry points. 7 | 8 | In this example we update the configuration programmatically, a more clever approach will be to 9 | identify the architect configuration for the secondary build and use the `tsconfig` in it to load the configuration we want. 10 | 11 | This will require auto-matching between secondary packages and architect project names, which is not direct but inferred. It might 12 | be better to use a configuration on the primary project with maps to child projects. 13 | 14 | With this, we ensure that we only build specific child packages and we use custom build configurations for each. It also serves 15 | as a good starting point to perform build only on affected secondary libs (NX). 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2020 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/tasks/ngc-cli-utils.ts: -------------------------------------------------------------------------------- 1 | export async function ngCompilerCli(): Promise { 2 | // This uses a dynamic import to load `@angular/compiler-cli` which may be ESM. 3 | // CommonJS code can load ESM code via a dynamic import. Unfortunately, TypeScript 4 | // will currently, unconditionally downlevel dynamic import into a require call. 5 | // require calls cannot load ESM code and will result in a runtime error. To workaround 6 | // this, a Function constructor is used to prevent TypeScript from changing the dynamic import. 7 | // Once TypeScript provides support for keeping the dynamic import this workaround can 8 | // be dropped. 9 | 10 | // Follow https://github.com/ng-packagr/ng-packagr/blob/8842ae3eb71747513bdea1a49f315ecabd012467/src/lib/utils/ng-compiler-cli.ts 11 | const compilerCliModule = await new Function(`return import('@angular/compiler-cli');`)(); 12 | 13 | // If it is not ESM then the functions needed will be stored in the `default` property. 14 | // This conditional can be removed when `@angular/compiler-cli` is ESM only. 15 | return compilerCliModule.readConfiguration ? compilerCliModule : compilerCliModule.default; 16 | } 17 | -------------------------------------------------------------------------------- /examples/update-tsconfig-for-secondary-entry-points/update-tsconfig-for-secondary-entry-points.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Example: Update tsconfig settings before compilation in secondary entry points. 3 | */ 4 | 5 | import { NgPackagerHooks, NgPackagerHooksContext } from 'ng-cli-packagr-tasks'; 6 | import { isEntryPoint } from 'ng-packagr/lib/ng-v5/nodes'; 7 | 8 | module.exports = function(ctx: NgPackagerHooksContext) { 9 | const hooks: NgPackagerHooks = { 10 | initTsConfig: { 11 | after: async taskContext => { 12 | for (const entry of taskContext.graph.entries()) { 13 | if (isEntryPoint(entry)) { 14 | if (entry.data.entryPoint.isSecondaryEntryPoint) { 15 | // UPDATE VALUES IN TSCONFIG: 16 | const tsConfig = entry.data.tsConfig; 17 | tsConfig.options.noImplicitAny = true 18 | 19 | // OR REPLACE IT ENTIRELY: 20 | entry.data.tsConfig = tsConfig; 21 | 22 | // NOTE: The tsconfig in `entry.data.tsConfig` is of type `ParsedConfiguration` in `@angular/compiler-cli`. 23 | // Its not the raw `tsconfig.json` style and include additional properties used the the compiler CLI. 24 | // Your best bet is to update rather the replace. 25 | } 26 | } 27 | } 28 | } 29 | }, 30 | }; 31 | return hooks; 32 | } 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ng-cli-packagr-tasks", 3 | "version": "14.2.2", 4 | "description": "Angular CLI Build Architect for ng-packagr with custom tasks and workflows", 5 | "main": "dist/index.js", 6 | "typings": "dist/index.d.ts", 7 | "builders": "builders.json", 8 | "scripts": { 9 | "build": "rm -rf dist && ./node_modules/.bin/tsc -p tsconfig.json && node publish-copy.mjs", 10 | "watch": "rm -rf dist && ./node_modules/.bin/tsc -w -p tsconfig.json" 11 | }, 12 | "author": "Shlomi Assaf ", 13 | "license": "MIT", 14 | "homepage": "https://github.com/shlomiassaf/ng-cli-packagr-tasks", 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/angularclass/ng-cli-packagr-tasks.git" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/shlomiassaf/ng-cli-packagr-tasks/issues" 21 | }, 22 | "keywords": [ 23 | "angular", 24 | "angular2", 25 | "angular4", 26 | "angular-library", 27 | "angular-components", 28 | "component-library", 29 | "typescript", 30 | "css", 31 | "scss", 32 | "html" 33 | ], 34 | "dependencies": { 35 | "globby": "^9.0.0" 36 | }, 37 | "peerDependencies": { 38 | "@angular-devkit/build-angular": "~14.2.0", 39 | "ng-packagr": "~14.2.0" 40 | }, 41 | "devDependencies": { 42 | "@angular-devkit/build-angular": "~14.2.0", 43 | "@angular/compiler": "~14.2.0", 44 | "@angular/compiler-cli": "~14.2.0", 45 | "@types/node": "18.7.1", 46 | "@types/semver": "^5.5.0", 47 | "ng-packagr": "~14.2.1", 48 | "tslib": "^2.2.0", 49 | "typescript": "~4.8.4" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /examples/copy-files-and-bump/README.md: -------------------------------------------------------------------------------- 1 | # Example: Copy files & bump version 2 | 3 | In this example we are using 2 built-in tasks: 4 | 5 | - copy-file 6 | - bump 7 | 8 | The `copy-file` tasks will copy files based on instructions in the `angular.json` in the same format as `assets` in browser builds. 9 | Data is set in `tasks.data.copyFile` (for the `copy-file` task) 10 | 11 | > The json structure is validated and will throw if invalid. 12 | 13 | The `bump` task can also accept instructions through the `tasks.data` object but instead we will use a command line argument to 14 | tell the task when to run and when not to run (no cli argument). 15 | 16 | ## Angular CLI config (partial from `angular.json`) 17 | 18 | ```json 19 | "architect": { 20 | "build": { 21 | "builder": "ng-cli-packagr-tasks:build", 22 | "options": { 23 | "tsConfig": "tsconfig.lib.json", 24 | "project": "ng-package.json", 25 | "tasks": { 26 | "config": "copy-files-and-bump.ts", 27 | "data": { 28 | "copyFile": { 29 | "assets": [ 30 | { 31 | "glob": "**/*.txt", 32 | "input": "src", 33 | "output": "dist" 34 | } 35 | ] 36 | } 37 | } 38 | } 39 | } 40 | } 41 | } 42 | ``` 43 | 44 | The configuration file (`copy-files-and-bump.ts`) is very simple, it just registers the tasks in the proper hook and phase. 45 | In out case, in the `writePackage` hook at the `before` phase. 46 | 47 | Finally, to run with bump: 48 | 49 | ```bash 50 | ng build my-cli-project --prod --tasksArgs="bump=major" 51 | ``` 52 | 53 | You can replace `major` with any release type available in the `semver` 2 spec: 54 | 55 | ```ts 56 | type ReleaseType = "major" | "premajor" | "minor" | "preminor" | "patch" | "prepatch" | "prerelease"; 57 | 58 | ``` -------------------------------------------------------------------------------- /src/build/schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "ng-packagr Target", 3 | "description": "ng-packagr target options for Build Architect.", 4 | "type": "object", 5 | "properties": { 6 | "project": { 7 | "type": "string", 8 | "description": "The file path of the package.json for distribution via npm." 9 | }, 10 | "tsConfig": { 11 | "type": "string", 12 | "description": "The file path of the TypeScript configuration file." 13 | }, 14 | "watch": { 15 | "type": "boolean", 16 | "description": "Run build when files change.", 17 | "default": false 18 | }, 19 | "tasks": { 20 | "type": "object", 21 | "properties": { 22 | "config": { 23 | "type": "string", 24 | "description": "A path to a module exporting the transform configuration. The module must implement 'NgPackagerTransformerHooksModule'. If the module is a TS file and there isn't any handler for the .ts extension, will try to require ts-node/register" 25 | }, 26 | "data": { 27 | "type": "object", 28 | "description": "An arbitrary object with data passed for transformers.", 29 | "additionalProperties": true 30 | }, 31 | "tsConfig": { 32 | "type": "string", 33 | "description": "Valid when the module in 'transformConfig' is a TS module. The full path for the TypeScript configuration file , relative to the current workspace, used to load the module in transformConfig." 34 | } 35 | } 36 | }, 37 | "tasksArgs": { 38 | "type": "string", 39 | "description": "An optional string that you can use to provide command line arguments to tasks. \n\n Use URL query string format.", 40 | "default": "" 41 | } 42 | }, 43 | "additionalProperties": true, 44 | "required": [ 45 | "project" 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /examples/node-library/README.md: -------------------------------------------------------------------------------- 1 | # Example: Node Library 2 | 3 | In this example we are using the built in **job** `nodeLib` and just to add some flavour we also perform a copy using the `copyFile` **job**. 4 | 5 | `nodeLib` will disable all angular related build steps (ts compilation, package.json definitions, bundles) and create a simple TS compilation 6 | using a provided TS configuration file (optional) or the project's tsconfig when not set. 7 | 8 | ## Angular CLI config (partial from `angular.json`) 9 | 10 | ```json 11 | "architect": { 12 | "build": { 13 | "builder": "ng-cli-packagr-tasks:build", 14 | "options": { 15 | "tsConfig": "tsconfig.lib.json", 16 | "project": "ng-package.json", 17 | "tasks": { 18 | "config": "copy-files-and-bump.ts", 19 | "data": { 20 | "nodeLib": { 21 | "tsConfig": "tsconfig.node-lib.json" 22 | }, 23 | "copyFile": { 24 | "assets": [ 25 | { 26 | "glob": "**/*.txt", 27 | "input": "src", 28 | "output": "dist" 29 | } 30 | ] 31 | } 32 | } 33 | } 34 | } 35 | } 36 | } 37 | ``` 38 | 39 | > Dont forget `module: "commonjs"` in the ts configuration file. 40 | 41 | The `ng-package.json` file does not change, similar to an angular package build it will hold instructions for the `entryFile`, destination folder etc... 42 | 43 | `ng-package.json` contains instructions specific to angular package builds, such instructions are ignored when using the node library job. (cssUrl, umdId, etc...). 44 | 45 | ## Post Processing 46 | 47 | Post processing tasks are not part of the scope, `nodeLib` will only compile the library and create a `package.json` for it. To perform other tasks add additional **jobs**. 48 | 49 | In this example we are copying files post creation. -------------------------------------------------------------------------------- /src/tasks/copy-file.json: -------------------------------------------------------------------------------- 1 | { 2 | "title": "Webpack browser schema for Build Facade.", 3 | "description": "Browser target options", 4 | "properties": { 5 | "copyFile": { 6 | "type": "object", 7 | "properties": { 8 | "assets": { 9 | "type": "array", 10 | "description": "List of static application assets.", 11 | "items": { 12 | "$ref": "#/definitions/assetPattern" 13 | } 14 | } 15 | }, 16 | "required": [ 17 | "assets" 18 | ] 19 | } 20 | }, 21 | "definitions": { 22 | "assetPattern": { 23 | "oneOf": [ 24 | { 25 | "type": "object", 26 | "properties": { 27 | "glob": { 28 | "type": "string", 29 | "description": "The pattern to match." 30 | }, 31 | "input": { 32 | "type": "string", 33 | "description": "The input directory path in which to apply 'glob'. Defaults to the project root." 34 | }, 35 | "ignore": { 36 | "description": "An array of globs to ignore.", 37 | "type": "array", 38 | "items": { 39 | "type": "string" 40 | } 41 | }, 42 | "output": { 43 | "type": "string", 44 | "description": "Absolute path within the output." 45 | }, 46 | "explicitFileName": { 47 | "type": "string", 48 | "description": "When set, copy the input file into a specific output file. Requires the input glob to return a single result." 49 | } 50 | }, 51 | "additionalProperties": false, 52 | "required": [ 53 | "glob", 54 | "output" 55 | ] 56 | }, 57 | { 58 | "type": "string" 59 | } 60 | ] 61 | } 62 | } 63 | } -------------------------------------------------------------------------------- /src/build/hook-registry.ts: -------------------------------------------------------------------------------- 1 | import { TaskPhases, NgPackagerHooks, NormalizedNgPackagerHooks } from './hooks'; 2 | import { normalizeHooks, normalizeTaskPhases, HOOK_PHASES, TRANSFORM_PROVIDER_MAP } from './utils'; 3 | import { JobMetadata, findJobMetadata, Type } from './job'; 4 | 5 | export class HookRegistry { 6 | private _jobs: JobMetadata[] = []; 7 | private _hooks: NormalizedNgPackagerHooks = {}; 8 | 9 | get hasSelfWatchJob(): boolean { 10 | return this._jobs.some( job => job.internalWatch ) 11 | } 12 | 13 | constructor(initialHooks?: NgPackagerHooks) { 14 | if (initialHooks) { 15 | this._hooks = normalizeHooks(initialHooks); 16 | } 17 | } 18 | 19 | register(job: Type): this; 20 | register(hook: T, handlers: NgPackagerHooks[T]): this; 21 | register(hookOrJob: T | Type, handlers?: NgPackagerHooks[T]): this { 22 | if (typeof hookOrJob === 'string') { 23 | const taskPhases = normalizeTaskPhases(handlers); 24 | for (const phase of HOOK_PHASES) { 25 | const handlers = taskPhases[phase]; 26 | if (handlers) { 27 | this.getHookPhase(hookOrJob, phase).push(...handlers) 28 | } 29 | } 30 | } else { 31 | const jobMeta = findJobMetadata(hookOrJob); 32 | if (!jobMeta) { 33 | throw new Error(`Unknown Job: ${hookOrJob}`); 34 | } 35 | this._jobs.push(jobMeta); 36 | const { hooks }= jobMeta; 37 | const hookNames: Array = Object.keys(TRANSFORM_PROVIDER_MAP) as any; 38 | 39 | for (const key of hookNames) { 40 | if (hooks[key]) { 41 | this.register(key, hooks[key]); 42 | } 43 | } 44 | } 45 | return this; 46 | } 47 | 48 | getHooks(): NormalizedNgPackagerHooks { 49 | return this._hooks; 50 | } 51 | 52 | getJobs(): JobMetadata[] { 53 | return this._jobs; 54 | } 55 | 56 | private getHookPhase(hook: T, phase: P): NormalizedNgPackagerHooks[T][P] { 57 | const hookMap = this._hooks[hook] || (this._hooks[hook] = {} as NormalizedNgPackagerHooks[T]); 58 | 59 | if (!hookMap[phase]) { 60 | hookMap[phase] = [] as NormalizedNgPackagerHooks[T][P]; 61 | } 62 | 63 | return hookMap[phase] 64 | } 65 | } -------------------------------------------------------------------------------- /src/tasks/bump.ts: -------------------------------------------------------------------------------- 1 | import { map, tap, switchMap } from 'rxjs/operators'; 2 | import * as Path from 'path'; 3 | import * as semver from 'semver'; 4 | import { parse as parseJson } from 'jsonc-parser'; 5 | import { normalize, virtualFs, JsonObject } from '@angular-devkit/core'; 6 | import * as log from 'ng-packagr/lib/utils/log'; 7 | 8 | import { EntryPointTaskContext, Job } from '../build'; 9 | 10 | const VALID_BUMPS: semver.ReleaseType[] = [ 11 | 'major', 12 | 'premajor', 13 | 'minor', 14 | 'preminor', 15 | 'patch', 16 | 'prepatch', 17 | 'prerelease' 18 | ]; 19 | 20 | declare module '../build/hooks' { 21 | interface NgPackagrBuilderTaskSchema { 22 | bump?: "major" | "premajor" | "minor" | "preminor" | "patch" | "prepatch" | "prerelease"; 23 | } 24 | } 25 | 26 | async function bumpTask(context: EntryPointTaskContext) { 27 | const bump = context.taskArgs('bump') as semver.ReleaseType; 28 | if (!bump || context.epNode.data.entryPoint.isSecondaryEntryPoint) { 29 | return; 30 | } 31 | 32 | if (VALID_BUMPS.indexOf(bump) === -1) { 33 | const err = new Error(`BumpTask: Invalid semver version bump, ${bump} is not a known semver release type`); 34 | log.error(err.message); 35 | throw err; 36 | } 37 | 38 | const { entryPoint } = context.epNode.data; 39 | 40 | const ver = semver.parse(entryPoint.packageJson.version); 41 | const oldVersion = ver.version; 42 | const newVersion = semver.inc(ver, bump); 43 | entryPoint.packageJson.version = newVersion; 44 | 45 | const { host } = context.context(); 46 | const packageJsonPath = normalize(Path.join(entryPoint.basePath, 'package.json')); 47 | await host.read(packageJsonPath) 48 | .pipe( 49 | map( buffer => virtualFs.fileBufferToString(buffer) ), 50 | map( str => parseJson(str, null, { allowTrailingComma: true }) as {} as JsonObject ), 51 | tap( packageJson => packageJson.version = newVersion ), 52 | switchMap( packageJson => { 53 | return host.write( 54 | packageJsonPath, 55 | virtualFs.stringToFileBuffer(JSON.stringify(packageJson, null, 2)) 56 | ); 57 | }), 58 | ) 59 | .toPromise(); 60 | 61 | log.msg(`Version bumped from ${oldVersion} to ${newVersion} (${bump})`); 62 | } 63 | 64 | @Job({ 65 | schema: Path.resolve(__dirname, 'bump.json'), 66 | selector: 'bump', 67 | hooks: { 68 | writePackage: { 69 | before: bumpTask 70 | } 71 | } 72 | }) 73 | export class Bump { } 74 | -------------------------------------------------------------------------------- /src/build/index.ts: -------------------------------------------------------------------------------- 1 | import * as FS from 'fs'; 2 | import { Observable, from } from 'rxjs'; 3 | import { switchMap, tap } from 'rxjs/operators'; 4 | 5 | import * as devKitCore from '@angular-devkit/core'; 6 | import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect'; 7 | import { executeNgPackagrBuilder as _execute, NgPackagrBuilderOptions } from '@angular-devkit/build-angular'; 8 | import * as ngPackagr from 'ng-packagr'; 9 | 10 | import { NgPackagerHooksModule, NgPackagerHooksContext } from './hooks'; 11 | import { createHookProviders } from './create-hook-provider'; 12 | import { createHooksContext, validateTypedTasks } from './utils'; 13 | import { HookRegistry } from './hook-registry'; 14 | 15 | export * from './hooks'; 16 | export * from './hook-registry'; 17 | export { Job, JobMetadata, Type } from './job'; 18 | export { ENTRY_POINT_STORAGE, EntryPointStorage } from './utils'; 19 | 20 | const DEFAULT_TSCONFIG_OPTIONS = { 21 | moduleResolution: 'node', 22 | module: 'commonjs', 23 | target: 'es6', 24 | lib: [ 25 | 'es2017', 26 | 'dom' 27 | ], 28 | }; 29 | 30 | async function buildRegistry(globalTasksContext: NgPackagerHooksContext): Promise { 31 | const root = globalTasksContext.root; 32 | const { tasks } = globalTasksContext.options; 33 | const transformerPath = tasks.config; 34 | const tPath = devKitCore.getSystemPath(devKitCore.resolve(root, devKitCore.normalize(transformerPath)) as any); 35 | if (FS.existsSync(tPath)) { 36 | if (/\.ts$/.test(tPath) && !require.extensions['.ts']) { 37 | const tsNodeOptions = {} as any; 38 | if (tasks.tsConfig) { 39 | tsNodeOptions.project = tasks.tsConfig; 40 | } else { 41 | tsNodeOptions.compilerOptions = DEFAULT_TSCONFIG_OPTIONS; 42 | } 43 | require('ts-node').register(tsNodeOptions); 44 | } 45 | const transformHooksModule: NgPackagerHooksModule = require(tPath); 46 | 47 | if (typeof transformHooksModule === 'function') { 48 | const registry = new HookRegistry(); 49 | await transformHooksModule(globalTasksContext, registry); 50 | return registry; 51 | } else { 52 | const registry = new HookRegistry(transformHooksModule); 53 | return registry; 54 | } 55 | }; 56 | return Promise.resolve(new HookRegistry()); 57 | } 58 | 59 | async function initRegistry(options: NgPackagrBuilderOptions, builderContext: BuilderContext) { 60 | const context = await createHooksContext(options, builderContext); 61 | const registry = await buildRegistry(context); 62 | return { context, registry }; 63 | } 64 | 65 | export function execute(options: NgPackagrBuilderOptions, context: BuilderContext): Observable { 66 | const { build, watch } = ngPackagr.NgPackagr.prototype; 67 | 68 | return from (initRegistry(options, context)) 69 | .pipe( 70 | switchMap( result => validateTypedTasks(result.registry.getJobs(), result.context).then( () => result ) ), 71 | tap( result => { 72 | const providers = createHookProviders(result.registry.getHooks(), result.context); 73 | ngPackagr.NgPackagr.prototype.build = function (this: ngPackagr.NgPackagr) { 74 | this.withProviders(providers); 75 | return build.call(this); 76 | } 77 | ngPackagr.NgPackagr.prototype.watch = function (this: ngPackagr.NgPackagr) { 78 | this.withProviders(providers); 79 | if (result.registry.hasSelfWatchJob) { 80 | return this.buildAsObservable(); 81 | } else { 82 | return watch.call(this); 83 | } 84 | } 85 | }), 86 | switchMap( () => _execute(options, context) ), 87 | tap( buildEvent => { 88 | ngPackagr.NgPackagr.prototype.build = build; 89 | ngPackagr.NgPackagr.prototype.watch = watch; 90 | }), 91 | ); 92 | } 93 | 94 | export default createBuilder & NgPackagrBuilderOptions>(execute); -------------------------------------------------------------------------------- /src/build/create-hook-provider.ts: -------------------------------------------------------------------------------- 1 | import * as qs from 'querystring'; 2 | import { pipe } from 'rxjs'; 3 | 4 | import { BuildGraph } from 'ng-packagr/lib/graph/build-graph'; 5 | import { TransformProvider } from 'ng-packagr/lib/graph/transform.di'; 6 | import { transformFromPromise, Transform } from 'ng-packagr/lib/graph/transform'; 7 | import { isEntryPointInProgress, EntryPointNode } from 'ng-packagr/lib/ng-package/nodes'; 8 | 9 | import { 10 | HookHandler, 11 | TaskContext, 12 | EntryPointTaskContext, 13 | NormalizedNgPackagerHooks, 14 | NgPackagerHooksContext, 15 | NgPackagrBuilderTaskSchema 16 | } from './hooks'; 17 | import { TRANSFORM_PROVIDER_MAP } from './utils'; 18 | 19 | const HOOK_HANDLERS: Array = ['initTsConfig', 'analyseSources']; 20 | 21 | class _TaskContext implements EntryPointTaskContext { 22 | readonly factoryInjections: T; 23 | 24 | /** 25 | * The main build graph 26 | */ 27 | graph: BuildGraph; 28 | 29 | epNode: EntryPointNode; 30 | 31 | private parsedTaskArgs: any; 32 | 33 | constructor(private readonly _context: NgPackagerHooksContext, factoryInjections: T, graph?: BuildGraph) { 34 | this.factoryInjections = factoryInjections; 35 | if (graph) { 36 | this.graph = graph; 37 | } 38 | } 39 | 40 | context(): NgPackagerHooksContext { 41 | return this._context as any; 42 | } 43 | 44 | taskArgs(key: string): string | undefined { 45 | if (!this.parsedTaskArgs) { 46 | const { tasksArgs } = this.context().options; 47 | this.parsedTaskArgs = qs.parse(tasksArgs || ''); 48 | } 49 | return this.parsedTaskArgs[key]; 50 | } 51 | } 52 | 53 | export function createHookProviders(hooksConfig: NormalizedNgPackagerHooks, 54 | globalTasksContext: NgPackagerHooksContext): TransformProvider[] { 55 | const providers: TransformProvider[] = []; 56 | const hookNames: Array = Object.keys(TRANSFORM_PROVIDER_MAP) as any; 57 | 58 | for (const key of hookNames) { 59 | if (hooksConfig[key]) { 60 | providers.push(createHookProvider(key, hooksConfig[key], globalTasksContext)); 61 | } 62 | } 63 | 64 | return providers; 65 | } 66 | 67 | export function createHookProvider(sourceHookName: T, 68 | hookConfig: NormalizedNgPackagerHooks[T], 69 | globalTasksContext: NgPackagerHooksContext): TransformProvider { 70 | const originalProvider = TRANSFORM_PROVIDER_MAP[sourceHookName]; 71 | 72 | if (!originalProvider) { 73 | throw new Error(`Invalid source hook name, ${sourceHookName} is not a recognized hook`); 74 | } 75 | 76 | const clonedProvider = { ...originalProvider }; 77 | 78 | clonedProvider.useFactory = (...args: any[]) => { 79 | const { before, replace, after } = hookConfig; 80 | const isEntryPointHandler = HOOK_HANDLERS.indexOf(sourceHookName) === -1; 81 | 82 | const taskContextFactory: (g: BuildGraph) => TaskContext = graph => { 83 | const taskContext = new _TaskContext(globalTasksContext, args, graph); 84 | if (isEntryPointHandler) { 85 | taskContext.epNode = graph.find(isEntryPointInProgress()) as EntryPointNode; 86 | } 87 | return taskContext; 88 | } 89 | 90 | const runners: Transform[] = [ 91 | ...createHookTransform(before || [], taskContextFactory), 92 | ...createHookTransform(replace || [], taskContextFactory), 93 | !replace && originalProvider.useFactory(...args), 94 | ...createHookTransform(after || [], taskContextFactory), 95 | ].filter( t => !!t ); 96 | 97 | return pipe(...runners as [Transform, Transform?, Transform?]); 98 | }; 99 | 100 | return clonedProvider; 101 | } 102 | 103 | function createHookTransform(tasksLike: Array>, 104 | taskContextFactory: (g: BuildGraph) => TaskContext): Transform[] { 105 | return tasksLike.map( handler => { 106 | return transformFromPromise( async graph => { 107 | return Promise.resolve(handler(taskContextFactory(graph))).then( g => g || graph ); 108 | }); 109 | }); 110 | } 111 | 112 | -------------------------------------------------------------------------------- /examples/api-generator/api-generator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Example: Create documentation after building package. 3 | * 4 | * This example doesn't render documentation, it will only create a JSON schema with all metadata required to build 5 | * documentation for the library. 6 | * 7 | * To extract TS type metadata we are using `@microsoft/api-extractor`. 8 | */ 9 | 10 | import { NgPackagerHooks, NgPackagerHooksContext, EntryPointTaskContext } from 'ng-cli-packagr-tasks'; 11 | 12 | module.exports = function(ctx: NgPackagerHooksContext) { 13 | async function writePackageTransformer(taskContext: EntryPointTaskContext) { 14 | const entryPointNode = taskContext.epNode; 15 | 16 | // `api-extractor` works by analyzing the `d.ts` declaration output created when compiling TS. 17 | // We need to pass the path to the root "typings" file (public_api) and this is stored here: 18 | const publicApiFilePath = entryPointNode.data.destinationFiles.declarations; 19 | 20 | // We need to pass a typescript configuration to the extractor. 21 | // We have one at `entryPointNode.data.tsConfig`, which is a a parsed configuration object (tsconfig.json after it was processed by TS). 22 | // We get it only for the `paths` which contain proper path mappings (important mostly in secondary packages). 23 | const tsConfig = entryPointNode.data.tsConfig; 24 | 25 | const tsConfigOptions = { 26 | include: [ publicApiFilePath ], 27 | exclude: ['libs', 'node_modules', 'tmp'], 28 | compilerOptions: { 29 | paths: JSON.parse(JSON.stringify(tsConfig.options.paths || [])) 30 | }, 31 | }; 32 | 33 | // We use the logger in the context (architect logger) and wrap it with 34 | // a interface that `api-extractor` can use... 35 | const logger = { 36 | logVerbose(message: string): void { ctx.logger.debug(message); }, 37 | logInfo(message: string): void { ctx.logger.info(message); }, 38 | logWarning(message: string): void { ctx.logger.warn(message); }, 39 | logError(message: string): void { ctx.logger.error(message); }, 40 | }; 41 | const apiPackage = getApiPackage(publicApiFilePath, tsConfigOptions, logger); 42 | 43 | const apiExtractorFilePath = Path.join(Path.dirname(publicApiFilePath), 'api-extractor.json'); 44 | apiPackage.saveToJsonFile(apiExtractorFilePath, { 45 | newlineConversion: NewlineKind.CrLf, 46 | ensureFolderExists: true 47 | }); 48 | } 49 | 50 | // Note that we create a new TS compilation without reusing the previous program. 51 | // We have access to it (entryPointNode.cache.oldPrograms) but it refers to the source files and not declaration (d.ts) files that the extractor wants.... 52 | const hooks: NgPackagerHooks = { 53 | writePackage: { 54 | after: writePackageTransformer 55 | }, 56 | }; 57 | return hooks; 58 | } 59 | 60 | 61 | 62 | 63 | /* PROGRAMMATICALLY CREATE API METADATA FOR DOCS USING `@microsoft/api-extractor` */ 64 | 65 | import * as Path from 'path'; 66 | import { NewlineKind } from '@microsoft/node-core-library'; 67 | import * as ts from '@microsoft/api-extractor/node_modules/typescript'; 68 | import { ILogger } from '@microsoft/api-extractor/lib/api/ILogger'; 69 | import { Collector } from '@microsoft/api-extractor/lib/collector/Collector'; 70 | import { ApiModelGenerator } from '@microsoft/api-extractor/lib/generators/ApiModelGenerator'; 71 | import { ApiPackage } from '@microsoft/api-extractor/lib/api/model/ApiPackage'; 72 | 73 | 74 | const TS_DEFAULT_CONFIG_OPTIONS: ts.CompilerOptions = { 75 | target: ts.ScriptTarget.ES5, 76 | module: ts.ModuleKind.ES2015, 77 | lib: [ "es2017", "dom" ], 78 | baseUrl: '.', 79 | rootDir: '.', 80 | } 81 | 82 | export function getApiPackage(entryPoint: string, tsConfigJson: any, logger: ILogger): ApiPackage { 83 | const compilerOptions = tsConfigJson.compilerOptions || {}; 84 | const parsedCommandLine: ts.ParsedCommandLine = ts.parseJsonConfigFileContent( 85 | { ...tsConfigJson, compilerOptions: { ...TS_DEFAULT_CONFIG_OPTIONS, ...compilerOptions } }, 86 | ts.sys, 87 | process.cwd() 88 | ); 89 | 90 | const program: ts.Program = ts.createProgram(parsedCommandLine.fileNames, parsedCommandLine.options); 91 | const rootDir: string | undefined = program.getCompilerOptions().rootDir; 92 | 93 | const collector: Collector = new Collector({ 94 | program: program as any, 95 | entryPointFile: Path.isAbsolute(entryPoint) || !rootDir ? entryPoint : Path.resolve(rootDir, entryPoint), 96 | logger, 97 | policies: {}, 98 | validationRules: {}, 99 | }); 100 | 101 | collector.analyze(); 102 | const modelBuilder: ApiModelGenerator = new ApiModelGenerator(collector); 103 | return modelBuilder.buildApiPackage(); 104 | } -------------------------------------------------------------------------------- /src/build/hooks.ts: -------------------------------------------------------------------------------- 1 | import { ParsedConfiguration } from '@angular/compiler-cli'; 2 | import { logging, Path, virtualFs, schema } from '@angular-devkit/core'; 3 | import { BuilderContext } from '@angular-devkit/architect'; 4 | import { NgPackagrBuilderOptions } from '@angular-devkit/build-angular'; 5 | import { BuildGraph } from 'ng-packagr/lib/graph/build-graph'; 6 | import { EntryPointNode } from 'ng-packagr/lib/ng-package/nodes'; 7 | import { HookRegistry } from './hook-registry'; 8 | 9 | /** 10 | * A context for hooks running at the initialization phase, when all entry points are discovered and all initial values are loaded. 11 | */ 12 | export interface TaskContext { 13 | 14 | /** 15 | * A tuple with injected objects passed to the factory of the transformer. 16 | */ 17 | factoryInjections: T; 18 | 19 | /** 20 | * The main build graph 21 | */ 22 | graph: BuildGraph; 23 | 24 | context(): NgPackagerHooksContext; 25 | 26 | taskArgs(key: string): string | undefined; 27 | } 28 | 29 | /** 30 | * A context for hook handlers running at the processing phase, where each entry point is being processed in a sequence, one after the other. 31 | */ 32 | export interface EntryPointTaskContext extends TaskContext { 33 | /** 34 | * The current entry point processed. 35 | */ 36 | epNode: EntryPointNode; 37 | } 38 | 39 | export type HookHandler = (taskContext: T) => (BuildGraph | void | Promise | Promise); 40 | 41 | export type TaskOrTasksLike = HookHandler | Array>; 42 | 43 | export interface TaskPhases { 44 | before?: TaskOrTasksLike; 45 | replace?: TaskOrTasksLike; 46 | after?: TaskOrTasksLike; 47 | } 48 | 49 | export interface NgPackagerHooks { 50 | initTsConfig?: TaskPhases>; 51 | analyseSources?: TaskPhases; 52 | entryPoint?: TaskPhases; 53 | compileNgc?: TaskPhases; 54 | writeBundles?: TaskPhases; 55 | writePackage?: TaskPhases; 56 | } 57 | 58 | export interface NgPackagerHooksContext { 59 | logger: logging.LoggerApi, 60 | root: Path; 61 | projectRoot: Path; 62 | sourceRoot: Path; 63 | builderContext: BuilderContext; 64 | options: NgPackagrBuilderOptions; 65 | host: virtualFs.Host, 66 | registry: schema.SchemaRegistry, 67 | } 68 | 69 | export type NgPackagerHooksModule 70 | = NgPackagerHooks 71 | | ((ctx: NgPackagerHooksContext, registry: HookRegistry) => void | Promise); 72 | 73 | export interface NgPackagrBuilderTaskSchema { } 74 | 75 | export interface NgPackagrBuilderTaskOptions { 76 | /** 77 | * A path to a module exporting the transform configuration. 78 | * 79 | * The module must implement `NgPackagerTransformerHooksModule` which means it must export (default) one of: 80 | * 81 | * - Direct transformer hook configuration (`NgPackagerTransformerHooks`) 82 | * - A function that returns a transformer hook configuration or a Promise. `() => NgPackagerTransformerHooks | Promise` 83 | * 84 | * If the module is a TS file and there isn't any handler for the .ts extension, will try to require ts-node/register 85 | * 86 | * Note that this module is executed in `node` runtime, if it's a TS module make sure the ts compiler configuration is appropriate. 87 | */ 88 | config?: string; 89 | 90 | /** 91 | * An arbitrary object with data passed for transformers. 92 | * Use this to passed configuration to transformers, for example a copy file instruction. 93 | */ 94 | data?: T; 95 | /** 96 | * Valid when the module in 'transformConfig' is a TS module. The full path for the TypeScript configuration file , relative to the current workspace, used to load the module in transformConfig. 97 | */ 98 | tsConfig?: string; 99 | } 100 | 101 | export type NgPackagrBuilderOptionsWithTasks 102 | = NgPackagrBuilderOptions & { tasks: NgPackagrBuilderTaskOptions }; 103 | 104 | declare module '@angular-devkit/build-angular/src/builders/ng-packagr/schema.d' { 105 | interface Schema { 106 | tasks?: NgPackagrBuilderTaskOptions; 107 | tasksArgs?: string; 108 | } 109 | } 110 | 111 | /** @internal */ 112 | export interface NormalizedTaskPhases { 113 | before?: Array>; 114 | replace?: Array>; 115 | after?: Array>; 116 | } 117 | 118 | /** @internal */ 119 | export interface NormalizedNgPackagerHooks { 120 | initTsConfig?: NormalizedTaskPhases>; 121 | analyseSources?: NormalizedTaskPhases; 122 | entryPoint?: NormalizedTaskPhases; 123 | compileNgc?: NormalizedTaskPhases; 124 | writeBundles?: NormalizedTaskPhases; 125 | writePackage?: NormalizedTaskPhases; 126 | } -------------------------------------------------------------------------------- /src/tasks/copy-file.ts: -------------------------------------------------------------------------------- 1 | import * as Path from 'path'; 2 | import * as FS from 'fs'; 3 | 4 | import * as globby from 'globby'; 5 | import { AssetPattern } from '@angular-devkit/build-angular'; 6 | import { normalizeAssetPatterns } from '@angular-devkit/build-angular/src/utils/normalize-asset-patterns'; 7 | import * as log from 'ng-packagr/lib/utils/log'; 8 | 9 | import { EntryPointTaskContext, Job } from '../build'; 10 | 11 | declare module '../build/hooks' { 12 | interface NgPackagrBuilderTaskSchema { 13 | copyFile: { 14 | assets: AssetPattern[]; 15 | } 16 | } 17 | } 18 | 19 | declare module '@angular-devkit/build-angular/src/builders/browser/schema.d' { 20 | interface AssetPatternClass { 21 | explicitFileName?: string; 22 | } 23 | } 24 | 25 | export interface CopyPattern { 26 | context: string; 27 | to: string; 28 | ignore: string[]; 29 | explicitFileName?: string; 30 | from: { 31 | glob: string; 32 | dot: boolean; 33 | }; 34 | } 35 | 36 | function buildCopyPatterns(root: string, assets: ReturnType< typeof normalizeAssetPatterns>): CopyPattern[] { 37 | return assets.map( asset => { 38 | 39 | // Resolve input paths relative to workspace root and add slash at the end. 40 | asset.input = Path.resolve(root, asset.input).replace(/\\/g, '/'); 41 | asset.input = asset.input.endsWith('/') ? asset.input : asset.input + '/'; 42 | asset.output = asset.output.endsWith('/') || asset.explicitFileName?.length > 0 ? asset.output : asset.output + '/'; 43 | 44 | if (asset.output.startsWith('..')) { 45 | const message = 'An asset cannot be written to a location outside of the output path.'; 46 | throw new Error(message); 47 | } 48 | 49 | return { 50 | context: asset.input, 51 | // Now we remove starting slash to make Webpack place it from the output root. 52 | to: asset.output.replace(/^\//, ''), 53 | ignore: asset.ignore, 54 | explicitFileName: asset.explicitFileName, 55 | from: { 56 | glob: asset.glob, 57 | dot: true, 58 | }, 59 | }; 60 | }); 61 | } 62 | 63 | function createCopyPatterns(assetPatterns: AssetPattern[], root: string, projectRoot: string, maybeSourceRoot: string) { 64 | 65 | const assets = normalizeAssetPatterns( 66 | assetPatterns.map(p => { 67 | if (!(typeof p == "string" || p instanceof String) && ((p.input?.length || 0) == 0)) 68 | { 69 | p.input = projectRoot; 70 | } 71 | return p; 72 | }), 73 | root, 74 | projectRoot, 75 | maybeSourceRoot, 76 | ); 77 | 78 | return buildCopyPatterns(root, assets); 79 | } 80 | 81 | async function getGlobEntries(copyPattern: CopyPattern, copyOptions: globby.GlobbyOptions) { 82 | const fullPattern = copyPattern.context + copyPattern.from.glob; 83 | const opts = { ...copyOptions, dot: copyPattern.from.dot }; 84 | 85 | return globby(fullPattern, opts); 86 | } 87 | 88 | async function executeCopyPattern(copyPattern: CopyPattern, 89 | copyOptions: globby.GlobbyOptions, 90 | root: string, 91 | onCopy?: (from: string, to: string) => void) { 92 | const entries = await getGlobEntries(copyPattern, copyOptions); 93 | 94 | if (copyPattern.explicitFileName?.length > 0 && entries.length > 1) 95 | { 96 | throw new Error(`Using 'explicitFileName' requires the glob to resolve to a single file. [Input]: ${copyPattern.context}`) 97 | } 98 | for (const entry of entries) { 99 | const cleanFilePath = entry.replace(copyPattern.context, ''); 100 | const to = Path.resolve(root, copyPattern.to, copyPattern.explicitFileName || cleanFilePath); 101 | const pathToFolder = Path.dirname(to); 102 | 103 | pathToFolder.split('/').reduce((p, folder) => { 104 | p += folder + '/'; 105 | 106 | if (!FS.existsSync(p)) { 107 | FS.mkdirSync(p); 108 | } 109 | 110 | return p; 111 | }, ''); 112 | 113 | FS.copyFileSync(entry, to); 114 | 115 | if (onCopy) { 116 | onCopy(entry, to); 117 | } 118 | } 119 | } 120 | 121 | async function executeCopyPatterns(copyPatterns: CopyPattern[], 122 | root: string, 123 | copyOptions?: globby.GlobbyOptions, 124 | onCopy?: (pattern: CopyPattern, from: string, to: string) => void) { 125 | const opts = copyOptions ? { ...copyOptions } : {}; 126 | for (const copyPattern of copyPatterns) { 127 | const singleOnCopy = onCopy 128 | ? (from: string, to: string) => onCopy(copyPattern, from, to) 129 | : undefined 130 | ; 131 | await executeCopyPattern(copyPattern, opts, root, singleOnCopy); 132 | } 133 | } 134 | 135 | async function copyFilesTask(context: EntryPointTaskContext) { 136 | 137 | const globalContext = context.context(); 138 | if (context.epNode.data.entryPoint.isSecondaryEntryPoint) { 139 | return; 140 | } 141 | 142 | const { builderContext, options, root } = globalContext; 143 | 144 | const copyPatterns = createCopyPatterns( 145 | options.tasks.data.copyFile.assets, 146 | root, 147 | globalContext.projectRoot, 148 | globalContext.sourceRoot, 149 | ); 150 | 151 | const copyOptions = { ignore: ['.gitkeep', '**/.DS_Store', '**/Thumbs.db'] }; 152 | const onCopy = (pattern: CopyPattern, from: string, to: string) => { 153 | log.success(` - from: ${from}`); 154 | log.success(` - to: ${to}`); 155 | }; 156 | 157 | log.info('Copying assets'); 158 | 159 | try { 160 | await executeCopyPatterns(copyPatterns, root, copyOptions, onCopy); 161 | } catch (err) { 162 | builderContext.logger.error(err.toString()); 163 | throw err; 164 | } 165 | } 166 | 167 | 168 | @Job({ 169 | schema: Path.resolve(__dirname, 'copy-file.json'), 170 | selector: 'copyFile', 171 | hooks: { 172 | writePackage: { 173 | before: copyFilesTask 174 | } 175 | } 176 | }) 177 | export class CopyFile { 178 | static readonly copyFilesTask = copyFilesTask; 179 | static readonly createCopyPatterns = createCopyPatterns; 180 | static readonly executeCopyPattern = executeCopyPattern; 181 | static readonly executeCopyPatterns = executeCopyPatterns; 182 | } -------------------------------------------------------------------------------- /src/build/utils.ts: -------------------------------------------------------------------------------- 1 | import { of, throwError } from 'rxjs'; 2 | import { switchMap, map, concatMap } from 'rxjs/operators'; 3 | 4 | import { parse as parseJson } from 'jsonc-parser'; 5 | import { resolve, normalize, virtualFs, JsonObject, workspaces, schema } from '@angular-devkit/core'; 6 | import { NodeJsSyncHost } from '@angular-devkit/core/node'; 7 | import { NgPackagrBuilderOptions } from '@angular-devkit/build-angular'; 8 | 9 | import { BuilderContext } from '@angular-devkit/architect'; 10 | import { TransformProvider } from 'ng-packagr/lib/graph/transform.di'; 11 | import { EntryPointNode } from 'ng-packagr/lib/ng-package/nodes'; 12 | import { INIT_TS_CONFIG_TRANSFORM } from 'ng-packagr/lib/ng-package/entry-point/init-tsconfig.di'; 13 | import { ANALYSE_SOURCES_TRANSFORM } from 'ng-packagr/lib/ng-package/entry-point/analyse-sources.di'; 14 | import { ENTRY_POINT_TRANSFORM } from 'ng-packagr/lib/ng-package/entry-point/entry-point.di'; 15 | import { COMPILE_NGC_TRANSFORM } from 'ng-packagr/lib/ng-package/entry-point/compile-ngc.di'; 16 | import { WRITE_BUNDLES_TRANSFORM } from 'ng-packagr/lib/ng-package/entry-point/write-bundles.di'; 17 | import { WRITE_PACKAGE_TRANSFORM } from 'ng-packagr/lib/ng-package/entry-point/write-package.di'; 18 | 19 | import { NgPackagerHooksContext } from './hooks'; 20 | import { 21 | NgPackagerHooks, 22 | NgPackagrBuilderTaskOptions, 23 | NgPackagrBuilderTaskSchema, 24 | TaskPhases, 25 | NormalizedNgPackagerHooks, 26 | NormalizedTaskPhases 27 | } from './hooks'; 28 | import { JobMetadata } from './job'; 29 | 30 | export const TRANSFORM_PROVIDER_MAP: Record = { 31 | initTsConfig: INIT_TS_CONFIG_TRANSFORM, 32 | analyseSources: ANALYSE_SOURCES_TRANSFORM, 33 | entryPoint: ENTRY_POINT_TRANSFORM, 34 | compileNgc: COMPILE_NGC_TRANSFORM, 35 | writeBundles: WRITE_BUNDLES_TRANSFORM, 36 | writePackage: WRITE_PACKAGE_TRANSFORM 37 | } 38 | 39 | export const HOOK_PHASES = ['before', 'replace', 'after'] as Array; 40 | 41 | export function normalizeHooks(hooks: NgPackagerHooks): NormalizedNgPackagerHooks { 42 | const hookNames: Array = Object.keys(TRANSFORM_PROVIDER_MAP) as any; 43 | 44 | for (const key of hookNames) { 45 | if (hooks[key]) { 46 | hooks[key] = normalizeTaskPhases(hooks[key]) as any; 47 | } 48 | } 49 | 50 | return hooks as NormalizedNgPackagerHooks; 51 | } 52 | 53 | export function normalizeTaskPhases(taskPhases: TaskPhases): NormalizedTaskPhases { 54 | for (const phase of HOOK_PHASES) { 55 | const taskOrTasksLike = taskPhases[phase]; 56 | if (taskOrTasksLike) { 57 | taskPhases[phase] = Array.isArray(taskOrTasksLike) 58 | ? taskOrTasksLike 59 | : [ taskOrTasksLike ] 60 | ; 61 | } 62 | } 63 | return taskPhases as NormalizedTaskPhases; 64 | } 65 | 66 | export function getTaskDataInput(jobMeta: JobMetadata, tasks: NgPackagrBuilderTaskOptions) { 67 | const data = tasks.data || {}; 68 | return { [jobMeta.selector]: data[jobMeta.selector] }; 69 | } 70 | 71 | export async function validateTypedTasks(jobs: JobMetadata[], context: NgPackagerHooksContext) { 72 | const tasks: NgPackagrBuilderTaskOptions = context.options.tasks; 73 | const allHooksPromises: Promise[] = []; 74 | 75 | const promises = jobs.map( taskMeta => { 76 | return context.host.read(normalize(taskMeta.schema)) 77 | .pipe( 78 | map(buffer => virtualFs.fileBufferToString(buffer)), 79 | map(str => parseJson(str, null, { allowTrailingComma: true }) as {} as JsonObject), 80 | switchMap( schemaJson => { 81 | const contentJson = getTaskDataInput(taskMeta, tasks); 82 | // JSON validation modifies the content, so we validate a copy of it instead. 83 | const contentJsonCopy = JSON.parse(JSON.stringify(contentJson)); 84 | 85 | return context.registry.compile(schemaJson) 86 | .pipe( 87 | concatMap(validator => validator(contentJsonCopy)), 88 | concatMap(validatorResult => { 89 | return validatorResult.success 90 | ? of(contentJsonCopy) 91 | : throwError(new schema.SchemaValidationException(validatorResult.errors)) 92 | ; 93 | }), 94 | ); 95 | }) 96 | ) 97 | .toPromise(); 98 | }); 99 | 100 | allHooksPromises.push(...promises); 101 | 102 | await Promise.all(allHooksPromises); 103 | } 104 | 105 | export interface EntryPointStorage { } 106 | 107 | // Used to mimic the `data` object in `EntryPointNode` so we can bind things to an entry point through the pipes 108 | // external to ng-packager. 109 | export const ENTRY_POINT_STORAGE = { 110 | ENTRY_POINT_DATA: new WeakMap(), 111 | get(node: EntryPointNode): EntryPointStorage | undefined { 112 | return this.ENTRY_POINT_DATA.get(node); 113 | }, 114 | merge(node: EntryPointNode, data: Partial): void { 115 | const currentData = this.ENTRY_POINT_DATA.get(node) || {} as any; 116 | Object.assign(currentData, data); 117 | this.ENTRY_POINT_DATA.set(node, currentData); 118 | }, 119 | delete(node: EntryPointNode): boolean { 120 | return this.ENTRY_POINT_DATA.delete(node); 121 | } 122 | } 123 | 124 | export async function createHooksContext(options: NgPackagrBuilderOptions, context: BuilderContext, host: virtualFs.Host<{}> = new NodeJsSyncHost()): Promise { 125 | const registry = new schema.CoreSchemaRegistry(); 126 | registry.addPostTransform(schema.transforms.addUndefinedDefaults); 127 | 128 | const workspaceRoot = normalize(context.workspaceRoot); 129 | const { workspace } = await workspaces.readWorkspace( 130 | workspaceRoot, 131 | workspaces.createWorkspaceHost(host), 132 | ); 133 | 134 | const projectName = context.target?.project ?? workspace.extensions['defaultProject']; 135 | 136 | if (!projectName) { 137 | throw new Error('Must either have a target from the context or a default project.'); 138 | } 139 | 140 | const project = workspace.projects.get(projectName); 141 | const projectRoot = resolve(workspaceRoot, normalize(project.root)); 142 | const projectSourceRoot = project.sourceRoot; 143 | const sourceRoot = projectSourceRoot 144 | ? resolve(workspaceRoot, normalize(projectSourceRoot)) 145 | : undefined 146 | ; 147 | 148 | return { 149 | logger: context.logger, 150 | root: workspaceRoot, 151 | projectRoot, 152 | sourceRoot, 153 | builderContext: context, 154 | options, 155 | host, 156 | registry, 157 | }; 158 | } -------------------------------------------------------------------------------- /src/tasks/node-lib.ts: -------------------------------------------------------------------------------- 1 | 2 | import * as Path from 'path'; 3 | import * as ts from 'typescript'; 4 | import { ngCompilerCli } from './ngc-cli-utils'; 5 | 6 | import { setDependenciesTsConfigPaths } from 'ng-packagr/lib/ts/tsconfig'; 7 | import { isEntryPoint, EntryPointNode } from 'ng-packagr/lib/ng-package/nodes'; 8 | // import { writePackageJson } from 'ng-packagr/lib/ng-package/entry-point/write-package.transform'; 9 | import { NgPackage } from 'ng-packagr/lib/ng-package/package'; 10 | import { ensureUnixPath } from 'ng-packagr/lib/utils/path'; 11 | import * as log from 'ng-packagr/lib/utils/log'; 12 | 13 | import { TaskContext, EntryPointTaskContext, Job, ENTRY_POINT_STORAGE, EntryPointStorage } from '../build'; 14 | 15 | declare module '../build/hooks' { 16 | interface NgPackagrBuilderTaskSchema { 17 | nodeLib: { 18 | /** 19 | * The file path of the TypeScript configuration file, specific for node library generation. 20 | * If not set will use the tsConfig of the project 21 | */ 22 | tsConfig?: string; 23 | 24 | /** 25 | * Compiler options overriding the one's in the file 26 | */ 27 | compilerOptions?: ts.CompilerOptions; 28 | } 29 | } 30 | } 31 | 32 | declare module '../build/utils' { 33 | interface EntryPointStorage { 34 | nodeLib: { 35 | tsConfig: Pick; 36 | watchProgram?: ts.WatchOfFilesAndCompilerOptions; 37 | } 38 | } 39 | } 40 | 41 | function readTsConfig(configFile: string): ts.ParsedCommandLine { 42 | const rawConfigRead = ts.readConfigFile(configFile, ts.sys.readFile); 43 | if (rawConfigRead.error) { 44 | return { 45 | options: {}, 46 | fileNames: [], 47 | errors: [ rawConfigRead.error ], 48 | }; 49 | } 50 | 51 | return ts.parseJsonConfigFileContent(rawConfigRead.config, ts.sys, Path.dirname(configFile), undefined, Path.basename(configFile)); 52 | } 53 | 54 | /** 55 | * Run after the original initTsConfig and load the user tsconfig override, if given or the root tsConfig file. 56 | * We reload the configuration from scratch and create configuration parameters for a simple tsc compilation without the angular compiler stuff. 57 | * We run "after" and not "replace" because there are a lot of areas depending on the angular compiler `ParsedConfiguration` object. 58 | */ 59 | async function initTsConfig(context: TaskContext<[import ('@angular/compiler-cli').ParsedConfiguration]>) { 60 | const globalContext = context.context(); 61 | const nodeLib = globalContext.options.tasks.data.nodeLib || {}; 62 | 63 | const tsConfigPath = (nodeLib && nodeLib.tsConfig) || globalContext.options.tsConfig; 64 | const parsedTsConfig = readTsConfig(tsConfigPath); 65 | 66 | const { exitCodeFromResult, formatDiagnostics } = await ngCompilerCli(); 67 | if (parsedTsConfig.errors.length > 0) { 68 | const exitCode = exitCodeFromResult(parsedTsConfig.errors); 69 | if (exitCode !== 0) { 70 | return Promise.reject(new Error(formatDiagnostics(parsedTsConfig.errors))); 71 | } 72 | } 73 | 74 | const entryPoints = context.graph.filter(isEntryPoint) as EntryPointNode[]; 75 | 76 | for (const entryPointNode of entryPoints) { 77 | const { entryPoint } = entryPointNode.data; 78 | log.debug(`Initializing tsconfig for ${entryPoint.moduleId}`); 79 | const rootDir = Path.dirname(entryPoint.entryFilePath); 80 | 81 | const userOverridingCompilerOptions = nodeLib.compilerOptions || {}; 82 | const overrideOptions: ts.CompilerOptions = { 83 | ...userOverridingCompilerOptions, 84 | rootDir, 85 | outDir: entryPoint.destinationPath 86 | }; 87 | 88 | const tsConfig: EntryPointStorage['nodeLib']['tsConfig'] = { 89 | rootNames: [ entryPoint.entryFilePath ], 90 | options: Object.assign(JSON.parse(JSON.stringify(parsedTsConfig.options)), overrideOptions), 91 | projectReferences: parsedTsConfig.projectReferences, 92 | }; 93 | 94 | ENTRY_POINT_STORAGE.merge(entryPointNode, { 95 | nodeLib: { tsConfig } 96 | }); 97 | } 98 | 99 | return Promise.resolve(context.graph); 100 | } 101 | 102 | /** 103 | * Replace the original compilation step, compile for node and not for angular. 104 | */ 105 | async function compilerNgc(context: EntryPointTaskContext) { 106 | const { epNode } = context; 107 | const nodeLibCache = ENTRY_POINT_STORAGE.get(epNode).nodeLib; 108 | 109 | if (context.context().options.watch) { 110 | if (nodeLibCache.watchProgram) { 111 | return; 112 | } 113 | 114 | const formatHost: ts.FormatDiagnosticsHost = { 115 | getCanonicalFileName: path => path, 116 | getCurrentDirectory: ts.sys.getCurrentDirectory, 117 | getNewLine: () => ts.sys.newLine 118 | }; 119 | 120 | const entryPoints = context.graph.filter(isEntryPoint) as EntryPointNode[]; 121 | 122 | // Add paths mappings for dependencies 123 | const ngParsedTsConfig = setDependenciesTsConfigPaths(epNode.data.tsConfig, entryPoints); 124 | const tsConfig: EntryPointStorage['nodeLib']['tsConfig'] = JSON.parse(JSON.stringify(nodeLibCache.tsConfig)); 125 | tsConfig.options.paths = ngParsedTsConfig.options.paths; 126 | 127 | const { formatDiagnostics } = await ngCompilerCli(); 128 | const host = ts.createWatchCompilerHost( 129 | tsConfig.rootNames as any, 130 | tsConfig.options, 131 | ts.sys, 132 | ts.createEmitAndSemanticDiagnosticsBuilderProgram, 133 | (diagnostic: ts.Diagnostic) => log.error(formatDiagnostics([diagnostic], formatHost)), 134 | (diagnostic: ts.Diagnostic) => log.msg(ts.formatDiagnostic(diagnostic, formatHost)), 135 | tsConfig.projectReferences, 136 | ); 137 | const program = ts.createWatchProgram(host); 138 | nodeLibCache.watchProgram = program; 139 | 140 | } else { 141 | const entryPoints = context.graph.filter(isEntryPoint) as EntryPointNode[]; 142 | 143 | // Add paths mappings for dependencies 144 | const ngParsedTsConfig = setDependenciesTsConfigPaths(epNode.data.tsConfig, entryPoints); 145 | const tsConfig: EntryPointStorage['nodeLib']['tsConfig'] = JSON.parse(JSON.stringify(nodeLibCache.tsConfig)); 146 | tsConfig.options.paths = ngParsedTsConfig.options.paths; 147 | 148 | const scriptTarget = tsConfig.options.target; 149 | const cache = epNode.cache; 150 | const oldProgram = cache.oldPrograms && (cache.oldPrograms[scriptTarget] as ts.Program | undefined); 151 | 152 | const program = ts.createProgram({ 153 | rootNames: tsConfig.rootNames, 154 | options: tsConfig.options, 155 | oldProgram, 156 | }); 157 | 158 | cache.oldPrograms = { ...cache.oldPrograms, [scriptTarget]: program }; 159 | 160 | const emitResult = program.emit(); 161 | const allDiagnostics = ts.getPreEmitDiagnostics(program).concat(emitResult.diagnostics); 162 | 163 | log.debug( 164 | `ngc program structure is reused: ${ 165 | oldProgram ? (oldProgram as any).structureIsReused : 'No old program' 166 | }` 167 | ); 168 | 169 | const { exitCodeFromResult, formatDiagnostics } = await ngCompilerCli(); 170 | const exitCode = exitCodeFromResult(allDiagnostics); 171 | if (exitCode !== 0) { 172 | throw new Error(formatDiagnostics(allDiagnostics)); 173 | } 174 | } 175 | } 176 | 177 | /** 178 | * Replace the original package.json create logic, write values for a node library. 179 | */ 180 | async function writePackage(context: EntryPointTaskContext) { 181 | const { tsConfig, entryPoint } = context.epNode.data; 182 | const ngPackage: NgPackage = context.graph.find(node => node.type === 'application/ng-package').data; 183 | 184 | log.info('Writing package metadata'); 185 | const relativeUnixFromDestPath = (filePath: string) => 186 | ensureUnixPath(Path.relative(entryPoint.destinationPath, filePath)); 187 | 188 | // TODO: This will map the entry file to it's emitted output path taking rootDir into account. 189 | // It might not be fully accurate, consider using the compiler host to create a direct map. 190 | const distEntryFile = Path.join(entryPoint.destinationPath, Path.relative(tsConfig.options.rootDir, entryPoint.entryFilePath)).replace(/\.ts$/, '.js'); 191 | const distDtsEntryFile = distEntryFile.replace(/\.js$/, '.d.ts'); 192 | 193 | // await writePackageJson(entryPoint, ngPackage, { 194 | // main: relativeUnixFromDestPath(distEntryFile), 195 | // typings: relativeUnixFromDestPath(distDtsEntryFile), 196 | // }); 197 | 198 | log.success(`Built ${entryPoint.moduleId}`); 199 | } 200 | 201 | @Job({ 202 | schema: Path.resolve(__dirname, 'node-lib.json'), 203 | selector: 'nodeLib', 204 | internalWatch: true, 205 | hooks: { 206 | initTsConfig: { 207 | after: initTsConfig 208 | }, 209 | compileNgc: { 210 | replace: compilerNgc 211 | }, 212 | writeBundles: { 213 | replace: [] 214 | }, 215 | writePackage: { 216 | replace: writePackage 217 | } 218 | } 219 | }) 220 | export class NodeLib { 221 | static readonly initTsConfig = initTsConfig; 222 | static readonly compilerNgc = compilerNgc; 223 | static readonly writePackage = writePackage; 224 | } 225 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Angular CLI Packagr Tasks 2 | 3 | Tasks & Workflow for ng-packagr. 4 | 5 | --- 6 | 7 | Version 9 support angular version 9 and above. 8 | 9 | For angular 8 or lower use version 4. 10 | 11 | For angular 7 or lower use version 3. 12 | 13 | --- 14 | 15 | ## TL;DR 16 | 17 | Hook into the build steps of `ng-packger` and add custom behaviors or change the built in behaviors. 18 | 19 | Examples: 20 | 21 | - Copy files after package build 22 | - Bump version (semver) 23 | - Build node libraries instead of angular libraries. 24 | 25 | ## How it works? 26 | 27 | For every package, `ng-packagr` will run several tasks through its' pipeline. 28 | This library exposes an API to hook into the pipeline, each step in `ng-packagr` has a unique hook in the API. 29 | 30 | To alter the behavior each hook is split into 3 **phases** before and/or after each task and even replacing the built-in task completely. 31 | 32 | For each hook/phase combination we can register a handler function (or an array of them) that will be called at that specific phase. 33 | The handler has access to a lot of data including `ng-packagr` API, architect API and more... 34 | 35 | There are 6 hooks: initTsConfig, analyseSources, entryPoint, compileNgc, writeBundles, writePackage. 36 | Because there are 3 phases for each hook (before, replace, after) there are 18 points of contact. 37 | 38 | The handlers are the most basic form of interaction, we can combine several handlers registered at specific points into a **Job**. 39 | 40 | A **Job** is just a collection of handlers that together perform an operation, for example creating a node-library instead of angular library. 41 | Jobs can also accept input (through `angular.json`), which are type safe as we run validation on them. 42 | 43 | The library comes with some built-in **jobs** but you can easily create your own. 44 | 45 | ## Install 46 | 47 | ```bash 48 | yarn add ng-cli-packagr-tasks -D 49 | ``` 50 | 51 | ## Configuration 52 | 53 | Here is a simple CLI configuration for a library (`angular.json`): 54 | 55 | ```json 56 | "architect": { 57 | "build": { 58 | "builder": "@angular-devkit/build-ng-packagr:build", 59 | "options": { 60 | "tsConfig": "tsconfig.lib.json", 61 | "project": "ng-package.json" 62 | }, 63 | "configurations": { 64 | "production": { 65 | "project": "ng-package.prod.json" 66 | } 67 | } 68 | } 69 | } 70 | ``` 71 | 72 | This will run the classic `ng-packagr` build process. 73 | 74 | **Let's update it a bit:** 75 | 76 | ```json 77 | "architect": { 78 | "build": { 79 | "builder": "ng-cli-packagr-tasks:build", 80 | "options": { 81 | "tsConfig": "tsconfig.lib.json", 82 | "project": "ng-package.json", 83 | "tasks": { 84 | "config": "ng-packagr.transformers.ts" 85 | } 86 | }, 87 | "configurations": { 88 | "production": { 89 | "project": "ng-package.prod.json" 90 | } 91 | } 92 | } 93 | } 94 | ``` 95 | 96 | We've introduces 2 changes: 97 | 98 | - The **builder** has changed from `@angular-devkit/build-ng-packagr:build` to `ng-cli-packagr-tasks:build`. 99 | - The property **tasks** was added, pointing to a configuration module where we can tweak the process. 100 | 101 | > Note that `ng-packagr` itself does not change, only the architect builder. 102 | 103 | The **tasks** object has additional properties which we can use to customize the process and provide configuration for 104 | tasks. We will cover this shortly, for now let's focus on the configuration module (`tasks.config`). 105 | 106 | ### Configuration module 107 | 108 | The configuration module is a simple JS (or TS) file that exports (default) the transformation instructions, there are 2 ways: 109 | 110 | - Direct transformer hook configuration (`NgPackagerHooks`) 111 | - A function. `(ctx: NgPackagerHooksContext, registry: HookRegistry) => void | Promise` 112 | 113 | Regardless of how you choose to export the instructions (function or object), the end result is always the `NgPackagerTransformerHooks`. 114 | When using functions you register handlers through the `HookRegistry`, which also allow registering **jobs**. 115 | 116 | > The only way to use **jobs** is through a function, which provide access to the `HookRegistry`. 117 | 118 | ### Packagr hooks (`NgPackagerTransformerHooks`) 119 | 120 | `ng-packagr` has several build steps, in a certain order, that together form the process of creating a library in the angular package format spec. 121 | `NgPackagerHooks` is a map of hooks within the packaging process that you can tap in to, each hook correspond to a specific packagr step. 122 | 123 | ```ts 124 | export interface NgPackagerHooks { 125 | initTsConfig?: TaskPhases>; 126 | analyseSources?: TaskPhases; 127 | entryPoint?: TaskPhases; 128 | compileNgc?: TaskPhases; 129 | writeBundles?: TaskPhases; 130 | writePackage?: TaskPhases; 131 | } 132 | ``` 133 | > The order which the tasks run reflect in the order of the properties above. 134 | 135 | For example, `compileNgc` will compile the library (TS -> JS, twice in 2 formats). 136 | 137 | > `ng-packager` has more tasks, running before `analyseSources` but they do not provide any value for customization. 138 | 139 | Each hook is split into 3 phases... 140 | 141 | ## Task phases (`TransformerHook`) 142 | 143 | For each hook there are 3 phases which you can register (all optional): **before**, **replace** and **after** 144 | 145 | ```ts 146 | export interface TaskPhases { 147 | before?: TaskOrTasksLike; 148 | replace?: TaskOrTasksLike; 149 | after?: TaskOrTasksLike; 150 | } 151 | ``` 152 | 153 | The order which the phases run are: **before** -> **replace** -> **after** 154 | 155 | The timeline is relative to the **replace** phase, which is where the original `ng-packagr` task runs. 156 | If you set a hook handler in **replace** the original task from `ng-packagr` **WILL NOT RUN**. 157 | 158 | > Do not set a handler in **replace** unless you really know what you are doing! 159 | 160 | Each phase accepts a single task or an array of tasks, for now let's define a task as a function that handles that hook: 161 | 162 | ```ts 163 | export type HookHandler = (taskContext: T) => (BuildGraph | void | Promise | Promise); 164 | ``` 165 | 166 | The hook is invoked with a `taskContext` parameter. 167 | 168 | The context holds metadata and APIs for the current task and globally for the entire process. 169 | 170 | ```ts 171 | /** 172 | * A context for hooks running at the initialization phase, when all entry points are discovered and all initial values are loaded. 173 | */ 174 | export interface TaskContext { 175 | /** 176 | * A tuple with injected objects passed to the factory of the transformer. 177 | */ 178 | factoryInjections: T; 179 | 180 | /** 181 | * The main build graph 182 | */ 183 | graph: BuildGraph; 184 | 185 | context(): NgPackagerHooksContext; 186 | 187 | taskArgs(key: string): string | undefined; 188 | } 189 | 190 | /** 191 | * A context for hook handlers running at the processing phase, where each entry point is being processed in a sequence, one after the other. 192 | */ 193 | export interface EntryPointTaskContext extends TaskContext { 194 | epNode: EntryPointNode; 195 | } 196 | ``` 197 | 198 | > `EntryPointTaskContext` handlers are called multiple times, once for every package (primary and secondary). `TaskContext` is called once before starting to process packages 199 | 200 | There are 2 types of tasks: 201 | 202 | - A simple function (`HookHandler`) 203 | - A job 204 | 205 | The first is just a function that implements (`HookHandler`), best suited for ad-hoc quick tasks. 206 | 207 | Job are more strict and organized, they usually require input and they also provide a schema to validate against that input. (see copy example below). A job can 208 | spread over several hooks. 209 | 210 | > The input for all typed tasks is always through `tasks.data` where each typed task has a "namespace" which is a property on the data object that points to it's own input object. 211 | 212 | The library comes with several built in jobs. 213 | 214 | You can review [the source code](/src/tasks) for some of the built-in jobs and build your own. 215 | 216 | ## Examples 217 | 218 | Before we dive into examples, it's important that we understand what information is available to us, provided by `ng-packagr`. 219 | 220 | Most of it is stored in `EntryPointNode`. The `EntryPointNode` object contains a lot of things. File path locations (sources, destinations), cached files, cache TS compiler programs and more... 221 | 222 | If we want to copy or move files, delete, build something etc, we need to know where the resources are located... 223 | 224 | There isn't much documentation, but [it is typed which should be enough](https://github.com/ng-packagr/ng-packagr/blob/master/src/lib/ng-v5/nodes.ts). 225 | 226 | - [Filtering build of packages (custom task)](/examples/filter-packages) 227 | - [Copy files and Bump version (built-in tasks)](/examples/copy-files-and-bump) 228 | - [Node Library (built-in tasks)](/examples/node-library) 229 | - [API Metadata generator](/examples/api-generator) 230 | - [Modify TS compilation settings in secondary entry points](/examples/update-tsconfig-for-secondary-entry-points) 231 | 232 | TODO: 233 | 234 | - Example on information in `EntryPointNode` (destination, metadata etc...) 235 | - Example for theme builder (e.g. scss) 236 | --------------------------------------------------------------------------------