UNPKG

@kv-systems/ng-packagr

Version:

Compile and package Angular libraries in Angular Package Format (APF)

272 lines (242 loc) 9.4 kB
import { DepGraph } from 'dependency-graph'; import { NEVER, Observable, catchError, concatMap, debounceTime, defaultIfEmpty, filter, finalize, from, map, of as observableOf, pipe, startWith, switchMap, takeLast, tap, } from 'rxjs'; import { createFileWatch } from '../file-system/file-watcher'; import { BuildGraph } from '../graph/build-graph'; import { STATE_DIRTY, STATE_DONE, STATE_IN_PROGRESS } from '../graph/node'; import { Transform } from '../graph/transform'; import { colors } from '../utils/color'; import { rmdir } from '../utils/fs'; import * as log from '../utils/log'; import { ensureUnixPath } from '../utils/path'; import { discoverPackages } from './discover-packages'; import { EntryPointNode, PackageNode, byEntryPoint, fileUrl, fileUrlPath, isEntryPoint, isFileUrl, isPackage, ngUrl, } from './nodes'; import { NgPackagrOptions } from './options.di'; /** * A transformation for building an npm package: * * - discoverPackages * - options * - initTsConfig * - analyzeTsSources (thereby extracting template and stylesheet files) * - for each entry point * - run the entryPontTransform * * @param project Project token, reference to `ng-package.json` * @param options ng-packagr options * @param initTsConfigTransform Transformation initializing the tsconfig of each entry point. * @param analyseSourcesTransform Transformation analyzing the typescript source files of each entry point. * @param entryPointTransform Transformation for asset rendering and compilation of a single entry point. */ export const packageTransformFactory = ( project: string, options: NgPackagrOptions, initTsConfigTransform: Transform, analyseSourcesTransform: Transform, entryPointTransform: Transform, ) => (source$: Observable<BuildGraph>): Observable<BuildGraph> => { log.info(`Building Angular Package`); const buildTransform = options.watch ? watchTransformFactory(project, options, analyseSourcesTransform, entryPointTransform) : buildTransformFactory(project, analyseSourcesTransform, entryPointTransform); const pkgUri = ngUrl(project); const ngPkg = new PackageNode(pkgUri); return source$.pipe( // Discover packages and entry points // Clean the primary dest folder (should clean all secondary sub-directory, as well) switchMap(async graph => { ngPkg.data = await discoverPackages({ project }); graph.put(ngPkg); const { dest, deleteDestPath } = ngPkg.data; if (deleteDestPath) { try { await rmdir(dest, { recursive: true }); } catch {} } const entryPoints = [ngPkg.data.primary, ...ngPkg.data.secondaries].map(entryPoint => { const { destinationFiles, moduleId } = entryPoint; const node = new EntryPointNode( ngUrl(moduleId), ngPkg.cache.sourcesFileCache, ngPkg.cache.moduleResolutionCache, ); node.data = { entryPoint, destinationFiles }; node.state = 'dirty'; ngPkg.dependsOn(node); return node; }); // Add entry points to graph return graph.put(entryPoints); }), // Initialize the tsconfig for each entry point initTsConfigTransform, // perform build buildTransform, finalize(() => { for (const node of ngPkg.dependents) { if (node instanceof EntryPointNode) { node.cache.stylesheetProcessor?.destroy(); } } }), ); }; const watchTransformFactory = (project: string, options: NgPackagrOptions, analyseSourcesTransform: Transform, entryPointTransform: Transform) => (source$: Observable<BuildGraph>): Observable<BuildGraph> => { const CompleteWaitingForFileChange = '\nCompilation complete. Watching for file changes...'; const FileChangeDetected = '\nFile change detected. Starting incremental compilation...'; const FailedWaitingForFileChange = '\nCompilation failed. Watching for file changes...'; return source$.pipe( switchMap(graph => { const { data, cache } = graph.find(isPackage); const { onFileChange, watcher } = createFileWatch([], [data.dest + '/'], options.poll); graph.watcher = watcher; return onFileChange.pipe( tap(fileChange => { const { filePath } = fileChange; const { sourcesFileCache } = cache; const cachedSourceFile = sourcesFileCache.get(filePath); const { declarationFileName } = cachedSourceFile || {}; const uriToClean = [filePath, declarationFileName].map(x => fileUrl(ensureUnixPath(x))); const nodesToClean = graph.filter(node => uriToClean.some(uri => uri === node.url)); if (!nodesToClean.length) { return; } const allNodesToClean = [ ...nodesToClean, // if a non ts file changes we need to clean up its direct dependees // this is mainly done for resources such as html and css ...nodesToClean.filter(node => !node.url.endsWith('.ts')).flatMap(node => [...node.dependees]), ]; // delete node that changes for (const { url } of allNodesToClean) { sourcesFileCache.delete(fileUrlPath(url)); } const potentialStylesResources = new Set<string>(); for (const { url } of allNodesToClean) { if (isFileUrl(url)) { potentialStylesResources.add(fileUrlPath(url)); } } for (const entryPoint of graph.filter(isEntryPoint)) { let isDirty = !!entryPoint.cache.stylesheetProcessor.invalidate(potentialStylesResources)?.length; isDirty ||= allNodesToClean.some(dependent => entryPoint.dependents.has(dependent)); if (isDirty) { entryPoint.state = STATE_DIRTY; for (const url of uriToClean) { entryPoint.cache.analysesSourcesFileCache.delete(fileUrlPath(url)); } } } }), debounceTime(100), tap(() => log.msg(FileChangeDetected)), startWith(undefined), map(() => graph), ); }), switchMap(graph => { return observableOf(graph).pipe( buildTransformFactory(project, analyseSourcesTransform, entryPointTransform), tap(() => log.msg(CompleteWaitingForFileChange)), catchError(error => { log.error(error); log.msg(FailedWaitingForFileChange); return NEVER; }), ); }), ); }; const buildTransformFactory = (project: string, analyseSourcesTransform: Transform, entryPointTransform: Transform) => (source$: Observable<BuildGraph>): Observable<BuildGraph> => { const startTime = Date.now(); const pkgUri = ngUrl(project); return source$.pipe( // Analyse dependencies and external resources for each entry point analyseSourcesTransform, // Next, run through the entry point transformation (assets rendering, code compilation) scheduleEntryPoints(entryPointTransform), tap(graph => { const ngPkg = graph.get(pkgUri); log.success('\n------------------------------------------------------------------------------'); log.success(`Built Angular Package - from: ${ngPkg.data.src} - to: ${ngPkg.data.dest}`); log.success('------------------------------------------------------------------------------'); const b = colors.bold; const w = colors.white; log.msg(w(`\nBuild at: ${b(new Date().toISOString())} - Time: ${b('' + (Date.now() - startTime))}ms\n`)); }), ); }; const scheduleEntryPoints = (epTransform: Transform): Transform => pipe( concatMap(graph => { // Calculate node/dependency depth and determine build order const depGraph = new DepGraph({ circular: false }); for (const node of graph.values()) { if (!isEntryPoint(node)) { continue; } // Remove `ng://` prefix for better error messages const from = node.url.substring(5); depGraph.addNode(from); for (const dep of node.dependents) { if (!isEntryPoint(dep)) { continue; } const to = dep.url.substring(5); depGraph.addNode(to); depGraph.addDependency(from, to); } } // The array index is the depth. const groups = depGraph.overallOrder().map(ngUrl); // Build entry points with lower depth values first. return from(groups).pipe( map((epUrl: string): EntryPointNode => graph.find(byEntryPoint().and(ep => ep.url === epUrl))), filter((entryPoint: EntryPointNode): boolean => entryPoint.state !== STATE_DONE), concatMap(ep => observableOf(ep).pipe( // Mark the entry point as 'in-progress' tap(entryPoint => (entryPoint.state = STATE_IN_PROGRESS)), map(() => graph), epTransform, ), ), takeLast(1), // don't use last as sometimes it this will cause 'no elements in sequence', defaultIfEmpty(graph), ); }), );