From 8b04d44c37bce19c61dc20794c49afc17d7ef43c Mon Sep 17 00:00:00 2001 From: David Herges Date: Sat, 20 Jan 2018 18:26:44 +0100 Subject: [PATCH] feat: enable tsconfig customization thru the programmatic API (#517) Closes #256 --- src/lib/ng-package-format/artefacts.ts | 2 +- src/lib/ng-v5/packagr.spec.ts | 26 ++++++-- src/lib/ng-v5/packagr.ts | 43 +++++++++---- src/lib/steps/build-ng-package.ts | 86 +++++++++++++++---------- src/lib/steps/entry-point-transforms.ts | 17 ++++- src/lib/steps/ngc-tsconfig.ts | 80 +++++++++++++++++++++++ src/lib/steps/ngc.ts | 42 +----------- 7 files changed, 199 insertions(+), 97 deletions(-) create mode 100644 src/lib/steps/ngc-tsconfig.ts diff --git a/src/lib/ng-package-format/artefacts.ts b/src/lib/ng-package-format/artefacts.ts index 523b76faa..6d8979cef 100644 --- a/src/lib/ng-package-format/artefacts.ts +++ b/src/lib/ng-package-format/artefacts.ts @@ -1,6 +1,6 @@ import * as path from 'path'; import * as ts from 'typescript'; -import { TsConfig } from '../steps/ngc'; +import { TsConfig } from '../steps/ngc-tsconfig'; import { NgEntryPoint } from './entry-point'; import { NgPackage } from './package'; diff --git a/src/lib/ng-v5/packagr.spec.ts b/src/lib/ng-v5/packagr.spec.ts index c9b94a858..b5d6c40f4 100644 --- a/src/lib/ng-v5/packagr.spec.ts +++ b/src/lib/ng-v5/packagr.spec.ts @@ -1,20 +1,38 @@ +import * as ng from '@angular/compiler-cli'; import { expect } from 'chai'; import { provideProject, PROJECT_TOKEN, ngPackagr, NgPackagr } from './packagr'; +import { DEFAULT_TS_CONFIG_TOKEN } from '../steps/ngc-tsconfig'; describe(`ngPackagr()`, () => { - it(`should return a NgPackagr instance`, () => { const foo = ngPackagr(); expect(foo).to.be.an.instanceOf(NgPackagr); }); - xit(`should return something with pre-configured defaults`, () => { - // TODO + it(`should have a default tsconfig`, () => { + const toBeTested = ngPackagr(); + const defaultTsConfigProvider = toBeTested['providers'].filter(p => (p as any).provide === DEFAULT_TS_CONFIG_TOKEN); + expect(defaultTsConfigProvider).to.have.length(1); + }); + + describe(`withTsConfig()`, () => { + it(`should return self instance for chaining`, () => { + const toBeTested = ngPackagr(); + const mockConfig = ('foo' as any) as ng.ParsedConfiguration; + expect(toBeTested.withTsConfig(mockConfig)).to.equal(toBeTested); + }); + it(`should override the default tsconfig provider`, () => { + const mockConfig = ('foo' as any) as ng.ParsedConfiguration; + const toBeTested = ngPackagr().withTsConfig(mockConfig); + const tsConfigProviders = toBeTested['providers'].filter(p => (p as any).provide === DEFAULT_TS_CONFIG_TOKEN); + + expect(tsConfigProviders).to.have.length(2); + expect((tsConfigProviders[1] as any).useValue).to.equal('foo'); + }); }); }); describe(`provideProject()`, () => { - it(`should return the ValueProvider`, () => { const provider = provideProject('foo'); expect(provider.provide).to.equal(PROJECT_TOKEN); diff --git a/src/lib/ng-v5/packagr.ts b/src/lib/ng-v5/packagr.ts index 0e4084f6c..b54ddca53 100644 --- a/src/lib/ng-v5/packagr.ts +++ b/src/lib/ng-v5/packagr.ts @@ -1,17 +1,28 @@ import { InjectionToken, Provider, ReflectiveInjector, ValueProvider } from 'injection-js'; -import { buildNgPackage } from '../steps/build-ng-package'; +import { BUILD_NG_PACKAGE_TOKEN, BUILD_NG_PACKAGE_PROVIDER, BuildCallSignature } from '../steps/build-ng-package'; +import { + TsConfig, + DEFAULT_TS_CONFIG_PROVIDER, + DEFAULT_TS_CONFIG_TOKEN, + PREPARE_TS_CONFIG_PROVIDER +} from '../steps/ngc-tsconfig'; +import { ENTRY_POINT_TRANSFORMS_PROVIDER } from '../steps/entry-point-transforms'; export class NgPackagr { - - constructor( - private providers: Provider[] - ) {} + constructor(private providers: Provider[]) {} public withProviders(providers: Provider[]): NgPackagr { - this.providers = [ - ...this.providers, - ...providers - ]; + this.providers = [...this.providers, ...providers]; + + return this; + } + + /** Overwrites the default TypeScript configuration. */ + public withTsConfig(defaultValues: TsConfig): NgPackagr { + this.providers.push({ + provide: DEFAULT_TS_CONFIG_TOKEN, + useValue: defaultValues + }); return this; } @@ -20,14 +31,20 @@ export class NgPackagr { const injector = ReflectiveInjector.resolveAndCreate(this.providers); const project = injector.get(PROJECT_TOKEN); + const buildNgPackage: BuildCallSignature = injector.get(BUILD_NG_PACKAGE_TOKEN); + return buildNgPackage({ project }); } - } -export const ngPackagr = (): NgPackagr => new NgPackagr([ - // TODO: default providers come here -]); +export const ngPackagr = (): NgPackagr => + new NgPackagr([ + // Add default providers to this list. + BUILD_NG_PACKAGE_PROVIDER, + ENTRY_POINT_TRANSFORMS_PROVIDER, + DEFAULT_TS_CONFIG_PROVIDER, + PREPARE_TS_CONFIG_PROVIDER + ]); export const PROJECT_TOKEN = new InjectionToken('ng.v5.project'); diff --git a/src/lib/steps/build-ng-package.ts b/src/lib/steps/build-ng-package.ts index 7f11d98a8..f6b21f212 100644 --- a/src/lib/steps/build-ng-package.ts +++ b/src/lib/steps/build-ng-package.ts @@ -1,3 +1,4 @@ +import { InjectionToken, FactoryProvider } from 'injection-js'; import * as path from 'path'; import { CliArguments } from '../commands/build.command'; import { NgArtefacts } from '../ng-package-format/artefacts'; @@ -5,44 +6,59 @@ import { NgPackage } from '../ng-package-format/package'; import { copyFiles } from '../util/copy'; import * as log from '../util/log'; import { rimraf } from '../util/rimraf'; +import { BuildStep } from '../deprecations'; import { discoverPackages } from './init'; -import { transformSources } from './entry-point-transforms'; +import { ENTRY_POINT_TRANSFORMS_TOKEN } from './entry-point-transforms'; // XX: should eventually become a BuildStep -export async function buildNgPackage(opts: CliArguments): Promise { - log.info(`Building Angular Package`); - - let ngPackage: NgPackage; - try { - // READ `NgPackage` from either 'package.json', 'ng-package.json', or 'ng-package.js' - ngPackage = await discoverPackages(opts); - - // clean the primary dest folder (should clean all secondary module directories as well) - await rimraf(ngPackage.dest); - - const artefacts = new NgArtefacts(ngPackage.primary, ngPackage); - await transformSources({ artefacts, entryPoint: ngPackage.primary, pkg: ngPackage }); - for (const secondary of ngPackage.secondaries) { - const artefacts = new NgArtefacts(secondary, ngPackage); - await transformSources({ artefacts, entryPoint: secondary, pkg: ngPackage }); - } +export function buildNgPackageFactory(entryPointTransforms: BuildStep) { - await copyFiles(`${ngPackage.src}/README.md`, ngPackage.dest); - await copyFiles(`${ngPackage.src}/LICENSE`, ngPackage.dest); - - // clean the working directory for a successful build only - await rimraf(ngPackage.workingDirectory); - log.success(`Built Angular Package! - - from: ${ngPackage.src} - - to: ${ngPackage.dest} - `); - } catch (error) { - // Report error messages and throw the error further up - log.error(error); - if (ngPackage) { - log.info(`Build failed. The working directory was not pruned. Files are stored at ${ngPackage.workingDirectory}.`); - } + return async function buildNgPackage(opts: CliArguments): Promise { + log.info(`Building Angular Package`); + + let ngPackage: NgPackage; + try { + // READ `NgPackage` from either 'package.json', 'ng-package.json', or 'ng-package.js' + ngPackage = await discoverPackages(opts); + + // clean the primary dest folder (should clean all secondary module directories as well) + await rimraf(ngPackage.dest); + + // Sequentially build entry points + const entryPoints = [ ngPackage.primary, ...ngPackage.secondaries ]; + for (const entryPoint of entryPoints) { + // Prepare artefacts. Will be populated by the entry point transformations + const artefacts = new NgArtefacts(ngPackage.primary, ngPackage); + await entryPointTransforms({ artefacts, entryPoint, pkg: ngPackage }); + } + + await copyFiles(`${ngPackage.src}/README.md`, ngPackage.dest); + await copyFiles(`${ngPackage.src}/LICENSE`, ngPackage.dest); - throw error; - } + // clean the working directory for a successful build only + await rimraf(ngPackage.workingDirectory); + log.success(`Built Angular Package! + - from: ${ngPackage.src} + - to: ${ngPackage.dest} + `); + } catch (error) { + // Report error messages and throw the error further up + log.error(error); + if (ngPackage) { + log.info(`Build failed. The working directory was not pruned. Files are stored at ${ngPackage.workingDirectory}.`); + } + + throw error; + } + }; } + +export type BuildCallSignature = (opts: CliArguments) => Promise; + +export const BUILD_NG_PACKAGE_TOKEN = new InjectionToken('ng.v5.buildNgPackage'); + +export const BUILD_NG_PACKAGE_PROVIDER: FactoryProvider = { + provide: BUILD_NG_PACKAGE_TOKEN, + useFactory: buildNgPackageFactory, + deps: [ ENTRY_POINT_TRANSFORMS_TOKEN ] +}; diff --git a/src/lib/steps/entry-point-transforms.ts b/src/lib/steps/entry-point-transforms.ts index fb5be07c2..bacf7fc05 100644 --- a/src/lib/steps/entry-point-transforms.ts +++ b/src/lib/steps/entry-point-transforms.ts @@ -1,11 +1,12 @@ import * as path from 'path'; +import { InjectionToken, FactoryProvider } from 'injection-js'; import { NgArtefacts } from '../ng-package-format/artefacts'; import { NgEntryPoint } from '../ng-package-format/entry-point'; import { NgPackage } from '../ng-package-format/package'; import { BuildStep } from '../deprecations'; import { writePackage } from '../steps/package'; import { processAssets } from '../steps/assets'; -import { ngc, prepareTsConfig, collectTemplateAndStylesheetFiles, inlineTemplatesAndStyles } from '../steps/ngc'; +import { ngc, collectTemplateAndStylesheetFiles, inlineTemplatesAndStyles } from '../steps/ngc'; import { minifyJsFile } from '../steps/uglify'; import { remapSourceMap, relocateSourceMapSources } from '../steps/sorcery'; import { flattenToFesm15, flattenToUmd } from '../steps/rollup'; @@ -14,14 +15,15 @@ import { copySourceFilesToDestination } from '../steps/transfer'; import * as log from '../util/log'; import { ensureUnixPath } from '../util/path'; import { rimraf } from '../util/rimraf'; +import { PREPARE_TS_CONFIG_TOKEN } from './ngc-tsconfig'; /** * Transforms TypeScript source files to Angular Package Format. * * @param entryPoint The entry point that will be transpiled to a set of artefacts. */ -export const transformSources: BuildStep = - async (args): Promise => { +export function transformSourcesFactory(prepareTsConfig: BuildStep) { + return async (args): Promise => { const { artefacts, entryPoint, pkg } = args; log.info(`Building from sources for entry point '${entryPoint.moduleId}'`); @@ -98,3 +100,12 @@ export const transformSources: BuildStep = log.success(`Built ${entryPoint.moduleId}`); } +} + +export const ENTRY_POINT_TRANSFORMS_TOKEN = new InjectionToken('ng.v5.entryPointTransforms'); + +export const ENTRY_POINT_TRANSFORMS_PROVIDER: FactoryProvider = { + provide: ENTRY_POINT_TRANSFORMS_TOKEN, + useFactory: transformSourcesFactory, + deps: [ PREPARE_TS_CONFIG_TOKEN ] +}; diff --git a/src/lib/steps/ngc-tsconfig.ts b/src/lib/steps/ngc-tsconfig.ts new file mode 100644 index 000000000..c232c2df2 --- /dev/null +++ b/src/lib/steps/ngc-tsconfig.ts @@ -0,0 +1,80 @@ +import * as ng from '@angular/compiler-cli'; +// XX: has or is using name 'ParsedConfiguration' ... but cannot be named +import { ParsedConfiguration } from '@angular/compiler-cli'; +import { InjectionToken, FactoryProvider } from 'injection-js'; +import * as path from 'path'; +import * as ts from 'typescript'; +import { BuildStep } from '../deprecations'; + +/** + * TypeScript configuration used internally (marker typer). + */ +export type TsConfig = ng.ParsedConfiguration; + +/** + * Reads the default TypeScript configuration. + */ +export function defaultTsConfigFactory() { + return ng.readConfiguration(path.resolve(__dirname, '..', 'conf', 'tsconfig.ngc.json')); +} + +export const DEFAULT_TS_CONFIG_TOKEN = new InjectionToken('ng.v5.defaultTsConfig'); + +export const DEFAULT_TS_CONFIG_PROVIDER: FactoryProvider = { + provide: DEFAULT_TS_CONFIG_TOKEN, + useFactory: defaultTsConfigFactory, + deps: [] +}; + +/** + * Prepares TypeScript Compiler and Angular Compiler options by overriding the default config + * with entry point-specific values. + */ +export const prepareTsConfigFactory: (def: TsConfig) => BuildStep = defaultTsConfig => ({ + artefacts, + entryPoint, + pkg +}) => { + const basePath = path.dirname(entryPoint.entryFilePath); + + // Resolve defaults from DI token + const tsConfig = { ...defaultTsConfig }; + + tsConfig.rootNames = [entryPoint.entryFilePath]; + tsConfig.options.flatModuleId = entryPoint.moduleId; + tsConfig.options.flatModuleOutFile = `${entryPoint.flatModuleFile}.js`; + tsConfig.options.basePath = basePath; + tsConfig.options.baseUrl = basePath; + tsConfig.options.rootDir = basePath; + tsConfig.options.outDir = artefacts.outDir; + tsConfig.options.genDir = artefacts.outDir; + + if (entryPoint.languageLevel) { + // ng.readConfiguration implicitly converts "es6" to "lib.es6.d.ts", etc. + tsConfig.options.lib = entryPoint.languageLevel.map(lib => `lib.${lib}.d.ts`); + } + + switch (entryPoint.jsxConfig) { + case 'preserve': + tsConfig.options.jsx = ts.JsxEmit.Preserve; + break; + case 'react': + tsConfig.options.jsx = ts.JsxEmit.React; + break; + case 'react-native': + tsConfig.options.jsx = ts.JsxEmit.ReactNative; + break; + default: + break; + } + + artefacts.tsConfig = tsConfig; +}; + +export const PREPARE_TS_CONFIG_TOKEN = new InjectionToken('ng.v5.prepareTsConfig'); + +export const PREPARE_TS_CONFIG_PROVIDER: FactoryProvider = { + provide: PREPARE_TS_CONFIG_TOKEN, + useFactory: prepareTsConfigFactory, + deps: [DEFAULT_TS_CONFIG_TOKEN] +}; diff --git a/src/lib/steps/ngc.ts b/src/lib/steps/ngc.ts index c2c024a9a..cbbd5cb26 100644 --- a/src/lib/steps/ngc.ts +++ b/src/lib/steps/ngc.ts @@ -14,47 +14,7 @@ import * as log from '../util/log'; // ... @link https://github.com/angular/angular/blob/24bf3e2a251634811096b939e61d63297934579e/packages/compiler-cli/src/main.ts#L36-L38 import { createEmitCallback } from '../util/ngc-patches'; import { componentTransformer } from '../util/ts-transformers'; - -/** TypeScript configuration used internally (marker typer). */ -export type TsConfig = ng.ParsedConfiguration; - -/** Prepares TypeScript Compiler and Angular Compiler option. */ -export const prepareTsConfig: BuildStep = - ({ artefacts, entryPoint, pkg }) => { - const basePath = path.dirname(entryPoint.entryFilePath); - - // Read the default configuration and overwrite package-specific options - const tsConfig = ng.readConfiguration(path.resolve(__dirname, '..', 'conf', 'tsconfig.ngc.json')); - tsConfig.rootNames = [ entryPoint.entryFilePath ]; - tsConfig.options.flatModuleId = entryPoint.moduleId; - tsConfig.options.flatModuleOutFile = `${entryPoint.flatModuleFile}.js`; - tsConfig.options.basePath = basePath; - tsConfig.options.baseUrl = basePath; - tsConfig.options.rootDir = basePath; - tsConfig.options.outDir = artefacts.outDir; - tsConfig.options.genDir = artefacts.outDir; - - if (entryPoint.languageLevel) { - // ng.readConfiguration implicitly converts "es6" to "lib.es6.d.ts", etc. - tsConfig.options.lib = entryPoint.languageLevel.map(lib => `lib.${lib}.d.ts`); - } - - switch (entryPoint.jsxConfig) { - case 'preserve': - tsConfig.options.jsx = ts.JsxEmit.Preserve; - break; - case 'react': - tsConfig.options.jsx = ts.JsxEmit.React; - break; - case 'react-native': - tsConfig.options.jsx = ts.JsxEmit.ReactNative; - break; - default: - break; - } - - artefacts.tsConfig = tsConfig; - } +import { TsConfig } from './ngc-tsconfig'; /** Transforms TypeScript AST */ const transformSources =