Angular CLI builderها
تعدادی از commandهای Angular CLI یک process پیچیده را روی کد شما اجرا میکنند؛ مثل build، test یا serve کردن application. این commandها از یک ابزار داخلی به نام Architect برای اجرای CLI builderها استفاده میکنند؛ builderهایی که ابزار دیگری مثل bundler، test runner یا server را invoke میکنند تا task مورد نظر انجام شود. Custom builderها میتوانند یک task کاملاً جدید انجام دهند، یا مشخص کنند کدام third-party tool توسط command موجود استفاده شود.
این document توضیح میدهد CLI builderها چطور با workspace configuration file integrate میشوند و نشان میدهد چطور میتوانید builder خودتان را بسازید.
CLI builderها
ابزار داخلی Architect کار را به handler functionهایی به نام builder delegate میکند. یک builder handler function دو argument دریافت میکند:
| Argument | Type |
|---|---|
options | JSONObject |
context | BuilderContext |
separation of concerns در اینجا همانند schematics است؛ schematics برای commandهای دیگری از CLI استفاده میشوند که code شما را لمس میکنند، مثل ng generate.
scheduler، builder handler function را با یک target configuration مشخص اجرا میکند.
- object مربوط به
optionsاز optionها و configuration کاربر CLI فراهم میشود، در حالی که object مربوط بهcontextبهصورت خودکار توسط CLI Builder API فراهم میشود. - علاوه بر contextual information، object مربوط به
contextدسترسی به scheduling method به نامcontext.scheduleTarget()را هم فراهم میکند.
builder handler function میتواند synchronous باشد و یک value برگرداند، asynchronous باشد و یک Promise برگرداند، یا watch کند و چند value برگرداند، یعنی یک Observable برگرداند. return valueها همیشه باید از type مربوط به BuilderOutput باشند. این object شامل یک فیلد Boolean به نام success و یک فیلد optional به نام error است که میتواند error message داشته باشد.
Angular چند builder فراهم میکند که CLI برای commandهایی مثل ng build و ng test از آنها استفاده میکند. Default target configurationهای این builderها و دیگر CLI builderهای built-in را میتوان در بخش "architect" مربوط به workspace configuration file، یعنی angular.json، پیدا و configure کرد. همچنین میتوانید با ساخت builderهای خودتان Angular را extend و customize کنید و آنها را مستقیماً با ng run CLI command اجرا کنید.
ساختار project مربوط به builder
یک builder داخل folderی بهشکل "project" قرار میگیرد که از نظر ساختار شبیه Angular workspace است؛ با global configuration fileها در top level و configuration مشخصتر داخل source folder همراه code fileهایی که behavior را تعریف میکنند. برای مثال، folder مربوط به myBuilder میتواند فایلهای زیر را داشته باشد.
| فایلها | هدف |
|---|---|
src/my-builder.ts | فایل source اصلی برای builder definition. |
src/my-builder.spec.ts | فایل source برای testها. |
src/schema.json | definition مربوط به input optionهای builder. |
builders.json | builder definition. |
package.json | Dependencyها. https://docs.npmjs.com/files/package.json را ببینید. |
tsconfig.json | TypeScript configuration. |
Builderها میتوانند روی npm منتشر شوند؛ Publishing your Library را ببینید.
ساخت یک builder
بهعنوان مثال، builderی بسازید که یک فایل را به location جدیدی copy کند. برای ساخت builder، از function مربوط به CLI Builder یعنی createBuilder() استفاده کنید و یک object از نوع Promise<BuilderOutput> برگردانید.
// #docplaster
// #docregion builder, builder-skeleton
import {BuilderContext, BuilderOutput, createBuilder} from '@angular-devkit/architect';
import {JsonObject} from '@angular-devkit/core';
// #enddocregion builder-skeleton
import {promises as fs} from 'fs';
// #docregion builder-skeleton
interface Options extends JsonObject {
source: string;
destination: string;
}
export default createBuilder(copyFileBuilder);
async function copyFileBuilder(options: Options, context: BuilderContext): Promise<BuilderOutput> {
// #enddocregion builder, builder-skeleton
// #docregion progress-reporting
context.reportStatus(`Copying ${options.source} to ${options.destination}.`);
// #docregion builder, handling-output
try {
// #docregion report-status
await fs.copyFile(options.source, options.destination);
// #enddocregion report-status
} catch (err) {
// #enddocregion builder
context.logger.error('Failed to copy file.');
// #docregion builder
return {
success: false,
error: (err as Error).message,
};
}
// #enddocregion builder, handling-output
context.reportStatus('Done.');
// #docregion builder
return {success: true};
// #enddocregion progress-reporting
// #docregion builder-skeleton
}
// #enddocregion builder, builder-skeletonحالا کمی logic به آن اضافه کنیم. کد زیر pathهای source و destination file را از user optionها میگیرد و فایل را از source به destination کپی میکند \(با استفاده از نسخه Promise function داخلی Node.js به نام copyFile()\). اگر copy operation شکست بخورد، یک error همراه پیامی درباره problem زیرین برمیگرداند.
// #docplaster
// #docregion builder, builder-skeleton
import {BuilderContext, BuilderOutput, createBuilder} from '@angular-devkit/architect';
import {JsonObject} from '@angular-devkit/core';
// #enddocregion builder-skeleton
import {promises as fs} from 'fs';
// #docregion builder-skeleton
interface Options extends JsonObject {
source: string;
destination: string;
}
export default createBuilder(copyFileBuilder);
async function copyFileBuilder(options: Options, context: BuilderContext): Promise<BuilderOutput> {
// #enddocregion builder, builder-skeleton
// #docregion progress-reporting
context.reportStatus(`Copying ${options.source} to ${options.destination}.`);
// #docregion builder, handling-output
try {
// #docregion report-status
await fs.copyFile(options.source, options.destination);
// #enddocregion report-status
} catch (err) {
// #enddocregion builder
context.logger.error('Failed to copy file.');
// #docregion builder
return {
success: false,
error: (err as Error).message,
};
}
// #enddocregion builder, handling-output
context.reportStatus('Done.');
// #docregion builder
return {success: true};
// #enddocregion progress-reporting
// #docregion builder-skeleton
}
// #enddocregion builder, builder-skeletonمدیریت output
بهصورت پیشفرض، copyFile() چیزی در standard output یا error مربوط به process چاپ نمیکند. اگر error رخ دهد، ممکن است سخت باشد بفهمیم builder دقیقاً هنگام رخ دادن problem قصد انجام چه کاری داشته است. با logging کردن اطلاعات اضافه با استفاده از Logger API، context بیشتری اضافه کنید. این کار همچنین اجازه میدهد خود builder در process جداگانهای اجرا شود، حتی اگر standard output و error deactivated باشند.
میتوانید یک instance از Logger را از context دریافت کنید.
// #docplaster
// #docregion builder, builder-skeleton
import {BuilderContext, BuilderOutput, createBuilder} from '@angular-devkit/architect';
import {JsonObject} from '@angular-devkit/core';
// #enddocregion builder-skeleton
import {promises as fs} from 'fs';
// #docregion builder-skeleton
interface Options extends JsonObject {
source: string;
destination: string;
}
export default createBuilder(copyFileBuilder);
async function copyFileBuilder(options: Options, context: BuilderContext): Promise<BuilderOutput> {
// #enddocregion builder, builder-skeleton
// #docregion progress-reporting
context.reportStatus(`Copying ${options.source} to ${options.destination}.`);
// #docregion builder, handling-output
try {
// #docregion report-status
await fs.copyFile(options.source, options.destination);
// #enddocregion report-status
} catch (err) {
// #enddocregion builder
context.logger.error('Failed to copy file.');
// #docregion builder
return {
success: false,
error: (err as Error).message,
};
}
// #enddocregion builder, handling-output
context.reportStatus('Done.');
// #docregion builder
return {success: true};
// #enddocregion progress-reporting
// #docregion builder-skeleton
}
// #enddocregion builder, builder-skeletonProgress و status reporting
CLI Builder API شامل ابزارهای progress و status reporting است که میتوانند برای بعضی functionها و interfaceها hint فراهم کنند.
برای report کردن progress، از method مربوط به context.reportProgress() استفاده کنید که current value، optional total، و status string را بهعنوان argument میگیرد. total میتواند هر عددی باشد. برای مثال، اگر میدانید چند فایل باید process کنید، total میتواند تعداد فایلها باشد و current باید تعداد فایلهای process شده تا آن لحظه باشد. status string بدون تغییر میماند مگر اینکه یک string value جدید پاس دهید.
در مثال ما، copy operation یا تمام میشود یا هنوز در حال اجراست؛ بنابراین نیازی به progress report نیست، اما میتوانید status را report کنید تا parent builderی که builder ما را صدا زده بداند چه اتفاقی در جریان است. از method مربوط به context.reportStatus() برای generate کردن status string با هر طولی استفاده کنید.
برای حذف status، یک string خالی پاس دهید.
// #docplaster
// #docregion builder, builder-skeleton
import {BuilderContext, BuilderOutput, createBuilder} from '@angular-devkit/architect';
import {JsonObject} from '@angular-devkit/core';
// #enddocregion builder-skeleton
import {promises as fs} from 'fs';
// #docregion builder-skeleton
interface Options extends JsonObject {
source: string;
destination: string;
}
export default createBuilder(copyFileBuilder);
async function copyFileBuilder(options: Options, context: BuilderContext): Promise<BuilderOutput> {
// #enddocregion builder, builder-skeleton
// #docregion progress-reporting
context.reportStatus(`Copying ${options.source} to ${options.destination}.`);
// #docregion builder, handling-output
try {
// #docregion report-status
await fs.copyFile(options.source, options.destination);
// #enddocregion report-status
} catch (err) {
// #enddocregion builder
context.logger.error('Failed to copy file.');
// #docregion builder
return {
success: false,
error: (err as Error).message,
};
}
// #enddocregion builder, handling-output
context.reportStatus('Done.');
// #docregion builder
return {success: true};
// #enddocregion progress-reporting
// #docregion builder-skeleton
}
// #enddocregion builder, builder-skeletonBuilder input
میتوانید یک builder را بهصورت غیرمستقیم از طریق commandهای CLI مثل ng build invoke کنید، یا مستقیماً با command مربوط به Angular CLI یعنی ng run. در هر دو حالت، باید inputهای required را فراهم کنید، اما میتوانید اجازه دهید inputهای دیگر از valueهایی استفاده کنند که برای یک target مشخص، توسط یک configuration، یا روی command line از قبل configure شدهاند.
Input validation
inputهای builder را در یک JSON schema مرتبط با همان builder تعریف میکنید. مشابه schematics، ابزار Architect مقدارهای input resolve شده را داخل یک object به نام options جمع میکند و typeهای آنها را قبل از پاس دادن به builder function در برابر schema validate میکند.
برای example builder ما، options باید یک JsonObject با دو key باشد: یک source و یک destination، که هر دو string هستند.
میتوانید schema زیر را برای type validation این valueها ارائه کنید.
{
"$schema": "http://json-schema.org/schema",
"type": "object",
"properties": {
"source": {
"type": "string"
},
"destination": {
"type": "string"
}
}
}برای اطلاعات بیشتر، JSON schemas website را ببینید.
برای link کردن implementation مربوط به builder با schema و name آن، باید یک فایل builder definition بسازید که بتوانید در package.json به آن point کنید.
فایلی به نام builders.json بسازید که شبیه این باشد:
{
"builders": {
"copy": {
"implementation": "./dist/my-builder.js",
"schema": "./src/schema.json",
"description": "Copies a file."
}
}
}در فایل package.json، key مربوط به builders را اضافه کنید که به Architect tool میگوید builder definition file ما را کجا پیدا کند.
{
"name": "@example/copy-file",
"version": "1.0.0",
"description": "Builder for copying files",
"builders": "builders.json",
"dependencies": {
"@angular/build": "^21.2.0"
}
}نام رسمی builder ما حالا @example/copy-file:copy است. بخش اول package name است و بخش دوم builder name، همانطور که در فایل builders.json مشخص شده.
این valueها روی options.source و options.destination قابل دسترسی هستند.
// #docplaster
// #docregion builder, builder-skeleton
import {BuilderContext, BuilderOutput, createBuilder} from '@angular-devkit/architect';
import {JsonObject} from '@angular-devkit/core';
// #enddocregion builder-skeleton
import {promises as fs} from 'fs';
// #docregion builder-skeleton
interface Options extends JsonObject {
source: string;
destination: string;
}
export default createBuilder(copyFileBuilder);
async function copyFileBuilder(options: Options, context: BuilderContext): Promise<BuilderOutput> {
// #enddocregion builder, builder-skeleton
// #docregion progress-reporting
context.reportStatus(`Copying ${options.source} to ${options.destination}.`);
// #docregion builder, handling-output
try {
// #docregion report-status
await fs.copyFile(options.source, options.destination);
// #enddocregion report-status
} catch (err) {
// #enddocregion builder
context.logger.error('Failed to copy file.');
// #docregion builder
return {
success: false,
error: (err as Error).message,
};
}
// #enddocregion builder, handling-output
context.reportStatus('Done.');
// #docregion builder
return {success: true};
// #enddocregion progress-reporting
// #docregion builder-skeleton
}
// #enddocregion builder, builder-skeletonTarget configuration
یک builder باید target تعریفشدهای داشته باشد که آن را با یک input configuration و project مشخص associate کند.
Targetها در CLI configuration file یعنی angular.json تعریف میشوند. یک target مشخص میکند از کدام builder استفاده شود، default options configuration آن چیست، و named alternative configurationها کداماند. Architect در Angular CLI از target definition برای resolve کردن input optionها برای یک run مشخص استفاده میکند.
فایل angular.json برای هر project یک section دارد، و بخش "architect" هر project، targetهای builderهایی را configure میکند که توسط CLI commandهایی مثل 'build'، 'test' و 'serve' استفاده میشوند. بهعنوان مثال، command مربوط به ng build بهصورت پیشفرض builder مربوط به @angular/build:application را برای انجام build task اجرا میکند و default option valueها را طبق چیزی که برای target مربوط به build در angular.json مشخص شده پاس میدهد.
{
"myApp": {
"...": "...",
"architect": {
"build": {
"builder": "@angular/build:application",
"options": {
"outputPath": "dist/myApp",
"index": "src/index.html",
"...": "..."
},
"configurations": {
"production": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.prod.ts"
}
],
"optimization": true,
"outputHashing": "all",
"...": "..."
}
}
},
"...": "..."
}
}
}command، مجموعه default optionهایی را که در بخش "options" مشخص شدهاند به builder پاس میدهد. اگر flag مربوط به --configuration=production را پاس دهید، از override valueهای مشخصشده در configuration مربوط به production استفاده میکند. overrideهای option بیشتر را بهصورت جداگانه روی command line مشخص کنید.
Target stringها
command عمومی CLI یعنی ng run اولین argument خود را بهشکل target string زیر میگیرد.
project:target[:configuration]| جزئیات | |
|---|---|
| project | نام Angular CLI projectی که target با آن associate شده است. |
| target | یک named builder configuration از بخش architect فایل angular.json. |
| configuration | اختیاری؛ نام یک configuration override مشخص برای target دادهشده، همانطور که در فایل angular.json تعریف شده است. |
اگر builder شما builder دیگری را صدا بزند، ممکن است لازم باشد یک target string پاس دادهشده را بخواند. این string را با استفاده از utility function مربوط به targetFromTargetString() از @angular-devkit/architect به object parse کنید.
Schedule و run
Architect builderها را بهصورت asynchronous اجرا میکند. برای invoke کردن یک builder، taskی را schedule میکنید تا وقتی همه configuration resolutionها کامل شدند اجرا شود.
builder function تا زمانی که scheduler یک control object از نوع BuilderRun برنگرداند اجرا نمیشود. CLI معمولاً taskها را با صدا زدن function مربوط به context.scheduleTarget() schedule میکند و سپس input optionها را با استفاده از target definition داخل فایل angular.json resolve میکند.
Architect input optionها را برای یک target مشخص با گرفتن default options object، سپس overwrite کردن valueها از configuration، و بعد overwrite کردن بیشتر valueها از overrides objectی که به context.scheduleTarget() پاس داده شده resolve میکند. برای Angular CLI، overrides object از command line argumentها ساخته میشود.
Architect مقدارهای option نهایی را در برابر schema مربوط به builder validate میکند. اگر inputها معتبر باشند، Architect context را میسازد و builder را اجرا میکند.
برای اطلاعات بیشتر Workspace Configuration را ببینید.
یک object به نام options را مستقیماً به method پاس میدهید و آن option valueها بدون adjustment اضافه در برابر schema مربوط به builder validate میشوند.
فقط method مربوط به context.scheduleTarget() است که configuration و overrideها را از طریق فایل angular.json resolve میکند.
Default architect configuration
بیایید یک فایل ساده angular.json بسازیم که target configurationها را در context قرار دهد.
میتوانید builder را روی npm منتشر کنید؛ Publishing your Library را ببینید، و آن را با command زیر نصب کنید:
npm install @example/copy-fileاگر با ng new builder-test یک project جدید بسازید، فایل generated مربوط به angular.json چیزی شبیه زیر خواهد بود؛ فقط با default builder configurationها.
{
"projects": {
"builder-test": {
"architect": {
"build": {
"builder": "@angular/build:application",
"options": {
"outputPath": "dist/builder-test",
"index": "src/index.html",
"main": "src/main.ts",
"polyfills": "src/polyfills.ts",
"tsConfig": "src/tsconfig.app.json"
},
"configurations": {
"production": {
"optimization": true,
"aot": true
}
}
}
}
}
}
}اضافه کردن target
یک target جدید اضافه کنید که builder ما را برای copy کردن یک فایل اجرا کند. این target به builder میگوید فایل package.json را copy کند.
- یک target section جدید به object مربوط به
architectبرای project خود اضافه میکنیم - targetی با نام
copy-packageاز builder ما استفاده میکند که آن را روی@example/copy-fileمنتشر کردهاید. - object مربوط به options مقدارهای default برای دو inputی که تعریف کردهاید فراهم میکند.
source- فایل موجودی که copy میکنید.destination- pathی که میخواهید فایل در آن copy شود.
{
"projects": {
"builder-test": {
"architect": {
"copy-package": {
"builder": "@example/copy-file:copy",
"options": {
"source": "package.json",
"destination": "package-copy.json"
}
}
// Existing targets...
}
}
}
}اجرای builder
برای اجرای builder ما با default configuration مربوط به target جدید، از CLI command زیر استفاده کنید.
ng run builder-test:copy-packageاین command فایل package.json را به package-copy.json copy میکند.
از command-line argumentها برای override کردن defaultهای configure شده استفاده کنید. برای مثال، برای اجرا با مقدار متفاوتی برای destination، از CLI command زیر استفاده کنید.
ng run builder-test:copy-package --destination=package-other.jsonاین command فایل را بهجای package-copy.json در package-other.json copy میکند. چون option مربوط به source را override نکردهاید، همچنان از default file یعنی package.json copy میکند.
Testing یک builder
برای builder خود از integration testing استفاده کنید تا بتوانید مثل این example، از Architect scheduler برای ساخت context استفاده کنید. در builder source directory، یک test file جدید به نام my-builder.spec.ts بسازید. test، instanceهای جدیدی از JsonSchemaRegistry برای schema validation، TestingArchitectHost برای implementation in-memory از ArchitectHost، و Architect میسازد.
اینجا مثالی از testی آمده که copy file builder را اجرا میکند. test از builder برای copy کردن فایل package.json استفاده میکند و validate میکند که محتوای فایل copied با source یکی باشد.
// #docregion
import {Architect} from '@angular-devkit/architect';
import {TestingArchitectHost} from '@angular-devkit/architect/testing';
import {schema} from '@angular-devkit/core';
import {promises as fs} from 'fs';
import {join} from 'path';
describe('Copy File Builder', () => {
let architect: Architect;
let architectHost: TestingArchitectHost;
beforeEach(async () => {
const registry = new schema.CoreSchemaRegistry();
registry.addPostTransform(schema.transforms.addUndefinedDefaults);
// TestingArchitectHost() takes workspace and current directories.
// Since we don't use those, both are the same in this case.
architectHost = new TestingArchitectHost(__dirname, __dirname);
architect = new Architect(architectHost, registry);
// This will either take a Node package name, or a path to the directory
// for the package.json file.
await architectHost.addBuilderFromPackage(join(__dirname, '..'));
});
it('can copy files', async () => {
// A "run" can have multiple outputs, and contains progress information.
const run = await architect.scheduleBuilder('@example/copy-file:copy', {
source: 'package.json',
destination: 'package-copy.json',
});
// The "result" member (of type BuilderOutput) is the next output.
const output = await run.result;
// Stop the builder from running. This stops Architect from keeping
// the builder-associated states in memory, since builders keep waiting
// to be scheduled.
await run.stop();
// Expect that the copied file is the same as its source.
const sourceContent = await fs.readFile('package.json', 'utf8');
const destinationContent = await fs.readFile('package-copy.json', 'utf8');
expect(destinationContent).toBe(sourceContent);
});
});
// #enddocregionمیتوانید با rename کردن my-builder.spec.ts به my-builder.spec.js از این نیاز جلوگیری کنید.
Watch mode
بیشتر builderها یک بار run میشوند و return میکنند. با این حال، این behavior با builderی که تغییرات را watch میکند، مثل devserver، کاملاً compatible نیست. Architect میتواند از watch mode پشتیبانی کند، اما چند نکته وجود دارد.
Architect تا زمانی که Observable complete شود subscribe میماند و اگر builder دوباره با همان argumentها schedule شود، ممکن است از آن reuse کند.
- برای استفاده با watch mode، یک builder handler function باید یک
Observableبرگرداند.
بعد از اینکه اجرا شد، میتواند وارد watch mode شود تا توسط external event trigger شود. اگر eventی آن را برای restart trigger کند، builder باید function مربوط به context.reportRunning() را اجرا کند تا به Architect بگوید دوباره در حال اجراست. این کار مانع میشود Architect در صورت schedule شدن run دیگر، builder را stop کند.
- builder باید بعد از هر execution همیشه یک object از نوع
BuilderOutputemit کند.
وقتی builder شما برای خروج از watch mode، BuilderRun.stop() را صدا میزند، Architect از Observable مربوط به builder unsubscribe میکند و teardown logic مربوط به builder را برای clean up صدا میزند. این behavior همچنین اجازه میدهد buildهای long-running stop و clean up شوند.
بهطور کلی، اگر builder شما یک external event را watch میکند، بهتر است run خود را به سه phase جدا کنید.
| Phaseها | جزئیات |
|---|---|
| Running | task در حال انجام، مثل invoke کردن compiler. این phase وقتی تمام میشود که compiler finish شود و builder شما یک object از نوع BuilderOutput emit کند. |
| Watching | بین دو run، یک external event stream را watch کنید. برای مثال، file system را برای هر change watch کنید. این phase وقتی تمام میشود که compiler restart شود و context.reportRunning() صدا زده شود. |
| Completion | یا task کاملاً complete شده است، مثل compilerی که باید چند بار run شود، یا builder run متوقف شده است، با استفاده از BuilderRun.stop(). Architect teardown logic را اجرا میکند و از Observable مربوط به builder unsubscribe میکند. |
Summary
CLI Builder API راهی فراهم میکند تا behavior مربوط به Angular CLI را با استفاده از builderها برای اجرای custom logic تغییر دهید.
- Builderها میتوانند synchronous یا asynchronous باشند، یک بار execute شوند یا external eventها را watch کنند، و میتوانند builderها یا targetهای دیگر را schedule کنند.
- Builderها option defaultهایی دارند که در configuration file مربوط به
angular.jsonمشخص میشوند؛ این مقدارها میتوانند توسط alternate configuration برای target و سپس توسط command line flagها overwrite شوند - تیم Angular توصیه میکند برای test کردن Architect builderها از integration test استفاده کنید. از unit testها برای validate کردن logicی استفاده کنید که builder اجرا میکند.
- اگر builder شما یک
Observableبرمیگرداند، باید builder را در teardown logic همانObservableclean up کند.