fix(angular): support dynamic imports in buildable libs (#5560)

This commit is contained in:
Leosvel Pérez Espinosa 2021-05-05 20:17:08 +01:00 committed by GitHub
parent 6aadf98fca
commit 939da0a1c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 374 additions and 144 deletions

View File

@ -5,6 +5,7 @@
* since these libraries will be compiled by the ngtsc. * since these libraries will be compiled by the ngtsc.
*/ */
import { InjectionToken } from 'injection-js';
import { import {
Transform, Transform,
transformFromPromise, transformFromPromise,
@ -13,7 +14,6 @@ import {
provideTransform, provideTransform,
TransformProvider, TransformProvider,
} from 'ng-packagr/lib/graph/transform.di'; } from 'ng-packagr/lib/graph/transform.di';
import { COMPILE_NGC_TOKEN } from 'ng-packagr/lib/ng-package/entry-point/compile-ngc.di';
import { import {
EntryPointNode, EntryPointNode,
isEntryPoint, isEntryPoint,
@ -26,7 +26,7 @@ import { setDependenciesTsConfigPaths } from 'ng-packagr/lib/ts/tsconfig';
import * as path from 'path'; import * as path from 'path';
import * as ts from 'typescript'; import * as ts from 'typescript';
export const compileNgcTransformFactory = ( export const nxCompileNgcTransformFactory = (
StylesheetProcessor: typeof StylesheetProcessorClass StylesheetProcessor: typeof StylesheetProcessorClass
): Transform => { ): Transform => {
return transformFromPromise(async (graph) => { return transformFromPromise(async (graph) => {
@ -73,8 +73,11 @@ export const compileNgcTransformFactory = (
}); });
}; };
export const NX_COMPILE_NGC_TOKEN = new InjectionToken<Transform>(
`nx.v1.compileNgc`
);
export const NX_COMPILE_NGC_TRANSFORM: TransformProvider = provideTransform({ export const NX_COMPILE_NGC_TRANSFORM: TransformProvider = provideTransform({
provide: COMPILE_NGC_TOKEN, provide: NX_COMPILE_NGC_TOKEN,
useFactory: compileNgcTransformFactory, useFactory: nxCompileNgcTransformFactory,
deps: [STYLESHEET_PROCESSOR_TOKEN], deps: [STYLESHEET_PROCESSOR_TOKEN],
}); });

View File

@ -4,23 +4,18 @@
* where Nx takes over with Nx specific functions * where Nx takes over with Nx specific functions
*/ */
import { COMPILE_NGC_TOKEN } from 'ng-packagr/lib/ng-package/entry-point/compile-ngc.di';
import {
NX_WRITE_BUNDLES_TRANSFORM,
NX_WRITE_BUNDLES_TRANSFORM_TOKEN,
} from './write-bundles';
import {
WRITE_PACKAGE_TRANSFORM,
WRITE_PACKAGE_TRANSFORM_TOKEN,
} from 'ng-packagr/lib/ng-package/entry-point/write-package.di';
import { InjectionToken, Provider } from 'injection-js'; import { InjectionToken, Provider } from 'injection-js';
import { Transform } from 'ng-packagr/lib/graph/transform'; import { Transform } from 'ng-packagr/lib/graph/transform';
import { import {
provideTransform, provideTransform,
TransformProvider, TransformProvider,
} from 'ng-packagr/lib/graph/transform.di'; } from 'ng-packagr/lib/graph/transform.di';
import { entryPointTransformFactory } from 'ng-packagr/lib/ng-package/entry-point/entry-point.transform'; import { NX_COMPILE_NGC_TOKEN, NX_COMPILE_NGC_TRANSFORM } from './compile-ngc';
import { NX_COMPILE_NGC_TRANSFORM } from './compile-ngc'; import { nxEntryPointTransformFactory } from './entry-point';
import {
NX_WRITE_PACKAGE_TRANSFORM,
NX_WRITE_PACKAGE_TRANSFORM_TOKEN,
} from './write-package.di';
export const NX_ENTRY_POINT_TRANSFORM_TOKEN = new InjectionToken<Transform>( export const NX_ENTRY_POINT_TRANSFORM_TOKEN = new InjectionToken<Transform>(
`nx.v1.entryPointTransform` `nx.v1.entryPointTransform`
@ -28,17 +23,12 @@ export const NX_ENTRY_POINT_TRANSFORM_TOKEN = new InjectionToken<Transform>(
export const NX_ENTRY_POINT_TRANSFORM: TransformProvider = provideTransform({ export const NX_ENTRY_POINT_TRANSFORM: TransformProvider = provideTransform({
provide: NX_ENTRY_POINT_TRANSFORM_TOKEN, provide: NX_ENTRY_POINT_TRANSFORM_TOKEN,
useFactory: entryPointTransformFactory, useFactory: nxEntryPointTransformFactory,
deps: [ deps: [NX_COMPILE_NGC_TOKEN, NX_WRITE_PACKAGE_TRANSFORM_TOKEN],
COMPILE_NGC_TOKEN,
NX_WRITE_BUNDLES_TRANSFORM_TOKEN,
WRITE_PACKAGE_TRANSFORM_TOKEN,
],
}); });
export const NX_ENTRY_POINT_PROVIDERS: Provider[] = [ export const NX_ENTRY_POINT_PROVIDERS: Provider[] = [
NX_ENTRY_POINT_TRANSFORM, NX_ENTRY_POINT_TRANSFORM,
NX_COMPILE_NGC_TRANSFORM, NX_COMPILE_NGC_TRANSFORM,
NX_WRITE_BUNDLES_TRANSFORM, NX_WRITE_PACKAGE_TRANSFORM,
WRITE_PACKAGE_TRANSFORM,
]; ];

View File

@ -0,0 +1,66 @@
/**
* Adapted from original ng-packagr source
*
* Remove writing bundles as they are not needed
* for incremental builds.
*/
import { logger } from '@nrwl/devkit';
import { STATE_DONE } from 'ng-packagr/lib/graph/node';
import { isInProgress } from 'ng-packagr/lib/graph/select';
import {
Transform,
transformFromPromise,
} from 'ng-packagr/lib/graph/transform';
import { byEntryPoint } from 'ng-packagr/lib/ng-package/nodes';
import { pipe } from 'rxjs';
/**
* A re-write of the `transformSources()` script that transforms an entry point from sources to distributable format.
*
* Sources are TypeScript source files accompanied by HTML templates and xCSS stylesheets.
* See the Angular Package Format for a detailed description of what the distributables include.
*
* The current transformation pipeline can be thought of as:
*
* - clean
* - compileTs
* - downlevelTs
* - relocateSourceMaps
* - writePackage
* - copyStagedFiles (esm, dts, metadata, sourcemaps)
* - writePackageJson
*
* The transformation pipeline is pluggable through the dependency injection system.
* Sub-transformations are passed to this factory function as arguments.
*
* @param compileTs Transformation compiling typescript sources to ES2015 modules.
* @param writePackage Transformation writing a distribution-ready `package.json` (for publishing to npm registry).
*/
export const nxEntryPointTransformFactory = (
compileTs: Transform,
writePackage: Transform
): Transform =>
pipe(
transformFromPromise(async (graph) => {
// Peek the first entry point from the graph
const entryPoint = graph.find(byEntryPoint().and(isInProgress));
logger.info(
'\n------------------------------------------------------------------------------'
);
logger.info(
`Building entry point '${entryPoint.data.entryPoint.moduleId}'`
);
logger.info(
'------------------------------------------------------------------------------'
);
}),
// TypeScript sources compilation
compileTs,
// After TypeScript: write package
writePackage,
transformFromPromise(async (graph) => {
const entryPoint = graph.find(byEntryPoint().and(isInProgress));
entryPoint.state = STATE_DONE;
})
);

View File

@ -1,121 +0,0 @@
/**
* Adapted from original ng-packagr
*
* Exclude the UMD bundling and minification
* which is not needed for incremental compilation
*/
import { InjectionToken } from 'injection-js';
import {
Transform,
transformFromPromise,
} from 'ng-packagr/lib/graph/transform';
import {
provideTransform,
TransformProvider,
} from 'ng-packagr/lib/graph/transform.di';
import {
EntryPointNode,
isEntryPoint,
isEntryPointInProgress,
} from 'ng-packagr/lib/ng-package/nodes';
import { NgEntryPoint } from 'ng-packagr/lib/ng-package/entry-point/entry-point';
import { rollupBundleFile } from 'ng-packagr/lib/flatten/rollup';
import * as log from 'ng-packagr/lib/utils/log';
import { DependencyList } from 'ng-packagr/lib/flatten/external-module-id-strategy';
import { BuildGraph } from 'ng-packagr/lib/graph/build-graph';
import { unique } from 'ng-packagr/lib/utils/array';
export const nxWriteBundlesTransform: Transform = transformFromPromise(
async (graph) => {
const entryPoint = graph.find(isEntryPointInProgress()) as EntryPointNode;
const {
destinationFiles,
entryPoint: ngEntryPoint,
tsConfig,
} = entryPoint.data;
const cache = entryPoint.cache;
// Add UMD module IDs for dependencies
const dependencyUmdIds = entryPoint
.filter(isEntryPoint)
.map((ep) => ep.data.entryPoint)
.reduce((prev, ep: NgEntryPoint) => {
prev[ep.moduleId] = ep.umdId;
return prev;
}, {});
const { fesm2015, esm2015 } = destinationFiles;
const opts = {
sourceRoot: tsConfig.options.sourceRoot,
amd: { id: ngEntryPoint.amdId },
umdModuleIds: {
...ngEntryPoint.umdModuleIds,
...dependencyUmdIds,
},
entry: esm2015,
dependencyList: getDependencyListForGraph(graph),
};
log.info('Bundling to FESM2015');
// @ts-ignore
cache.rollupFESMCache = await rollupBundleFile({
...opts,
moduleName: ngEntryPoint.moduleId,
format: 'es',
dest: fesm2015,
// @ts-ignore
cache: cache.rollupFESMCache,
});
}
);
/** Get all list of dependencies for the entire 'BuildGraph' */
function getDependencyListForGraph(graph: BuildGraph): DependencyList {
// We need to do this because if A dependency on bundled B
// And A has a secondary entry point A/1 we want only to bundle B if it's used.
// Also if A/1 depends on A we don't want to bundle A thus we mark this a dependency.
const dependencyList: DependencyList = {
dependencies: [],
bundledDependencies: [],
};
for (const entry of graph.filter(isEntryPoint)) {
const {
bundledDependencies = [],
dependencies = {},
peerDependencies = {},
} = entry.data.entryPoint.packageJson;
dependencyList.bundledDependencies = unique(
dependencyList.bundledDependencies.concat(bundledDependencies)
);
dependencyList.dependencies = unique(
dependencyList.dependencies.concat(
Object.keys(dependencies),
Object.keys(peerDependencies),
entry.data.entryPoint.moduleId
)
);
}
if (dependencyList.bundledDependencies.length) {
log.warn(
`Inlining of 'bundledDependencies' has been deprecated in version 5 and will be removed in future versions.` +
'\n' +
`List the dependency in the 'peerDependencies' section instead.`
);
}
return dependencyList;
}
export const NX_WRITE_BUNDLES_TRANSFORM_TOKEN = new InjectionToken<Transform>(
`nx.v1.writeBundlesTransform`
);
export const NX_WRITE_BUNDLES_TRANSFORM: TransformProvider = provideTransform({
provide: NX_WRITE_BUNDLES_TRANSFORM_TOKEN,
useFactory: () => nxWriteBundlesTransform,
});

View File

@ -0,0 +1,15 @@
import { InjectionToken } from 'injection-js';
import { Transform } from 'ng-packagr/lib/graph/transform';
import {
provideTransform,
TransformProvider,
} from 'ng-packagr/lib/graph/transform.di';
import { nxWritePackageTransform } from './write-package';
export const NX_WRITE_PACKAGE_TRANSFORM_TOKEN = new InjectionToken<Transform>(
`nx.v1.writePackageTransform`
);
export const NX_WRITE_PACKAGE_TRANSFORM: TransformProvider = provideTransform({
provide: NX_WRITE_PACKAGE_TRANSFORM_TOKEN,
useFactory: () => nxWritePackageTransform,
});

View File

@ -0,0 +1,276 @@
/**
* Adapted from original ng-packagr
*
* Change the package.json metadata to only use
* the ESM2015 output as it's the only one generated.
*/
import { logger } from '@nrwl/devkit';
import { Node } from 'ng-packagr/lib/graph/node';
import {
Transform,
transformFromPromise,
} from 'ng-packagr/lib/graph/transform';
import { NgEntryPoint } from 'ng-packagr/lib/ng-package/entry-point/entry-point';
import {
EntryPointNode,
fileUrl,
isEntryPointInProgress,
isPackage,
PackageNode,
} from 'ng-packagr/lib/ng-package/nodes';
import { NgPackage } from 'ng-packagr/lib/ng-package/package';
import {
copyFile,
exists,
rimraf,
stat,
writeFile,
} from 'ng-packagr/lib/utils/fs';
import { globFiles } from 'ng-packagr/lib/utils/glob';
import { ensureUnixPath } from 'ng-packagr/lib/utils/path';
import * as path from 'path';
export const nxWritePackageTransform: Transform = transformFromPromise(
async (graph) => {
const entryPoint: EntryPointNode = graph.find(isEntryPointInProgress());
const ngEntryPoint: NgEntryPoint = entryPoint.data.entryPoint;
const ngPackageNode: PackageNode = graph.find(isPackage);
const ngPackage = ngPackageNode.data;
const { destinationFiles } = entryPoint.data;
const ignorePaths: string[] = [
'.gitkeep',
'**/.DS_Store',
'**/Thumbs.db',
'**/node_modules/**',
`${ngPackage.dest}/**`,
];
if (ngPackage.assets.length && !ngEntryPoint.isSecondaryEntryPoint) {
const assetFiles: string[] = [];
// COPY ASSET FILES TO DESTINATION
logger.log('Copying assets');
try {
for (let asset of ngPackage.assets) {
asset = path.join(ngPackage.src, asset);
if (await exists(asset)) {
const stats = await stat(asset);
if (stats.isFile()) {
assetFiles.push(asset);
continue;
}
if (stats.isDirectory()) {
asset = path.join(asset, '**/*');
}
}
const files = await globFiles(asset, {
ignore: ignorePaths,
cache: ngPackageNode.cache.globCache,
dot: true,
nodir: true,
});
if (files.length) {
assetFiles.push(...files);
}
}
for (const file of assetFiles) {
const relativePath = path.relative(ngPackage.src, file);
const destination = path.resolve(ngPackage.dest, relativePath);
const nodeUri = fileUrl(ensureUnixPath(file));
let node = graph.get(nodeUri);
if (!node) {
node = new Node(nodeUri);
graph.put(node);
}
entryPoint.dependsOn(node);
await copyFile(file, destination);
}
} catch (error) {
throw error;
}
}
// 6. WRITE PACKAGE.JSON
try {
logger.info('Writing package metadata');
const relativeUnixFromDestPath = (filePath: string) =>
ensureUnixPath(path.relative(ngEntryPoint.destinationPath, filePath));
const isIvy = !!entryPoint.data.tsConfig.options.enableIvy;
await writePackageJson(
ngEntryPoint,
ngPackage,
{
module: relativeUnixFromDestPath(destinationFiles.esm2015),
esm2015: relativeUnixFromDestPath(destinationFiles.esm2015),
typings: relativeUnixFromDestPath(destinationFiles.declarations),
// Ivy doesn't generate metadata files
metadata: isIvy
? undefined
: relativeUnixFromDestPath(destinationFiles.metadata),
// webpack v4+ specific flag to enable advanced optimizations and code splitting
sideEffects: ngEntryPoint.sideEffects,
},
isIvy
);
} catch (error) {
throw error;
}
logger.info(`Built ${ngEntryPoint.moduleId}`);
return graph;
}
);
/**
* Creates and writes a `package.json` file of the entry point used by the `node_module`
* resolution strategies.
*
* #### Example
*
* A consumer of the entry point depends on it by `import {..} from '@my/module/id';`.
* The module id `@my/module/id` will be resolved to the `package.json` file that is written by
* this build step.
* The properties `main`, `module`, `typings` (and so on) in the `package.json` point to the
* flattened JavaScript bundles, type definitions, (...).
*
* @param entryPoint An entry point of an Angular package / library
* @param additionalProperties Additional properties, e.g. binary artefacts (bundle files), to merge into `package.json`
*/
async function writePackageJson(
entryPoint: NgEntryPoint,
pkg: NgPackage,
additionalProperties: { [key: string]: string | boolean | string[] },
isIvy: boolean
): Promise<void> {
// set additional properties
const packageJson = { ...entryPoint.packageJson, ...additionalProperties };
// read tslib version from `@angular/compiler` so that our tslib
// version at least matches that of angular if we use require('tslib').version
// it will get what installed and not the minimum version nor if it is a `~` or `^`
// this is only required for primary
if (!entryPoint.isSecondaryEntryPoint) {
if (
!packageJson.peerDependencies?.tslib &&
!packageJson.dependencies?.tslib
) {
const {
peerDependencies: angularPeerDependencies = {},
dependencies: angularDependencies = {},
} = require('@angular/compiler/package.json');
const tsLibVersion =
angularPeerDependencies.tslib || angularDependencies.tslib;
if (tsLibVersion) {
packageJson.dependencies = {
...packageJson.dependencies,
tslib: tsLibVersion,
};
}
} else if (packageJson.peerDependencies?.tslib) {
logger.warn(
`'tslib' is no longer recommended to be used as a 'peerDependencies'. Moving it to 'dependencies'.`
);
packageJson.dependencies = {
...(packageJson.dependencies || {}),
tslib: packageJson.peerDependencies.tslib,
};
delete packageJson.peerDependencies.tslib;
}
}
// Verify non-peerDependencies as they can easily lead to duplicate installs or version conflicts
// in the node_modules folder of an application
const allowedList = pkg.allowedNonPeerDependencies.map(
(value) => new RegExp(value)
);
try {
checkNonPeerDependencies(packageJson, 'dependencies', allowedList);
} catch (e) {
await rimraf(entryPoint.destinationPath);
throw e;
}
// Removes scripts from package.json after build
if (packageJson.scripts) {
if (pkg.keepLifecycleScripts !== true) {
logger.info(
`Removing scripts section in package.json as it's considered a potential security vulnerability.`
);
delete packageJson.scripts;
} else {
logger.warn(
`You enabled keepLifecycleScripts explicitly. The scripts section in package.json will be published to npm.`
);
}
}
if (isIvy && !entryPoint.isSecondaryEntryPoint) {
const scripts = packageJson.scripts || (packageJson.scripts = {});
scripts.prepublishOnly =
'node --eval "console.error(\'' +
'ERROR: Trying to publish a package that has been compiled by Ivy. This is not allowed.\\n' +
'Please delete and rebuild the package, without compiling with Ivy, before attempting to publish.\\n' +
'\')" ' +
'&& exit 1';
}
// keep the dist package.json clean
// this will not throw if ngPackage field does not exist
delete packageJson.ngPackage;
const packageJsonPropertiesToDelete = [
'stylelint',
'prettier',
'browserslist',
'devDependencies',
'jest',
'workspaces',
'husky',
];
for (const prop of packageJsonPropertiesToDelete) {
if (prop in packageJson) {
delete packageJson[prop];
logger.info(`Removing ${prop} section in package.json.`);
}
}
packageJson.name = entryPoint.moduleId;
await writeFile(
path.join(entryPoint.destinationPath, 'package.json'),
JSON.stringify(packageJson, undefined, 2)
);
}
function checkNonPeerDependencies(
packageJson: Record<string, unknown>,
property: string,
allowed: RegExp[]
) {
if (!packageJson[property]) {
return;
}
for (const dep of Object.keys(packageJson[property])) {
if (!allowed.some((regex) => regex.test(dep))) {
logger.warn(
`Distributing npm packages with '${property}' is not recommended. Please consider adding ${dep} to 'peerDependencies' or remove it from '${property}'.`
);
throw new Error(
`Dependency ${dep} must be explicitly allowed using the "allowedNonPeerDependencies" option.`
);
}
}
}

View File

@ -7,6 +7,7 @@ const IGNORE_MATCHES = {
'@angular-devkit/architect', '@angular-devkit/architect',
'@angular-devkit/build-angular', '@angular-devkit/build-angular',
'@angular-devkit/core', '@angular-devkit/core',
'@angular/compiler',
'@angular/compiler-cli', '@angular/compiler-cli',
'@angular/core', '@angular/core',
'@angular/router', '@angular/router',