feat(bazel): support ts_library targets as entry-points for ng_package (#32610)

Within an Angular package, it can happen that there are
entry-points which do not contain features that belong into
an `@NgModule` or need metadata files to be generated.

For example: the `cdk`, `cdk/testing` and `cdk/coercion`
entry-points. Besides other entry-points in the `cdk`
package, those entry-points do not need metadata to
be generated and no not use the `ng_module` rule.

Currently the "ng_package" rule properly picks up such
entry-points and builds bundles, does downleveling etc.
The only thing it misses is that no `package.json` files
are generated for the entry-point. This means that consumers
will not be able to use these entry-points built with "ts_library"
(except accessing the individual bundlings explicitly).

The "ng_package" rule should follow the full APF specification
for such entry-points. Partially building bundles and doing the
downleveling is confusing and a breaking issue.

The motifivation of supporting this (besides making the
rule behavior consistent; the incomplete output is not
acceptable), is that using the "ng_module" rule does
not make sense to be used for non-Angular entry-points.

Especially since it depends on Angular packages to
be specified as Bazel action inputs just to compile
vanilla TypeScript with `@angular/compiler-cli`.

PR Close #32610
This commit is contained in:
Paul Gschwendtner
2019-09-11 22:21:34 +02:00
committed by Kara Erickson
parent be13e8b1e4
commit 217db9b216
14 changed files with 1195 additions and 33 deletions

View File

@ -33,6 +33,11 @@ load("//packages/bazel/src:external.bzl", "FLAT_DTS_FILE_SUFFIX")
load("//packages/bazel/src:esm5.bzl", "esm5_outputs_aspect", "esm5_root_dir", "flatten_esm5")
load("//packages/bazel/src/ng_package:collect-type-definitions.bzl", "collect_type_definitions")
# Prints a debug message if "--define=VERBOSE_LOGS=true" is specified.
def _debug(vars, *args):
if "VERBOSE_LOGS" in vars.keys():
print("[ng_package.bzl]", *args)
_DEFAULT_NG_PACKAGER = "@npm//@angular/bazel/bin:packager"
# Convert from some-dash-case to someCamelCase
@ -98,7 +103,7 @@ WELL_KNOWN_GLOBALS = {p: _global_name(p) for p in [
# TODO(gregmagolan): clean this up
_DEPSET_TYPE = "depset"
def _rollup(ctx, bundle_name, rollup_config, entry_point, inputs, js_output, format = "es", package_name = "", include_tslib = False):
def _rollup(ctx, bundle_name, rollup_config, entry_point, inputs, js_output, format = "es", module_name = "", include_tslib = False):
map_output = ctx.actions.declare_file(js_output.basename + ".map", sibling = js_output)
args = ctx.actions.args()
@ -107,9 +112,9 @@ def _rollup(ctx, bundle_name, rollup_config, entry_point, inputs, js_output, for
args.add("--input", entry_point)
args.add("--output.file", js_output)
args.add("--output.format", format)
if package_name:
args.add("--output.name", _global_name(package_name))
args.add("--amd.id", package_name)
if module_name:
args.add("--output.name", _global_name(module_name))
args.add("--amd.id", module_name)
# After updating to build_bazel_rules_nodejs 0.27.0+, rollup has been updated to v1.3.1
# which tree shakes @__PURE__ annotations and const variables which are later amended by NGCC.
@ -228,22 +233,70 @@ def _ng_package_impl(ctx):
# - ng_module rules in the deps (they have an "angular" provider)
# - in this package or a subpackage
# - those that have a module_name attribute (they produce flat module metadata)
flat_module_metadata = []
collected_entry_points = []
# Name given in the package.json name field, eg. @angular/core/testing
package_name = ""
deps_in_package = [d for d in ctx.attr.deps if d.label.package.startswith(ctx.label.package)]
for dep in deps_in_package:
# Module name of the current entry-point. eg. @angular/core/testing
module_name = ""
# Intentionally evaluates to empty string for the main entry point
entry_point = dep.label.package[len(ctx.label.package) + 1:]
# Extract the "module_name" from either "ts_library" or "ng_module". Both
# set the "module_name" in the provider struct.
if hasattr(dep, "module_name"):
package_name = dep.module_name
module_name = dep.module_name
if hasattr(dep, "angular") and hasattr(dep.angular, "flat_module_metadata"):
flat_module_metadata.append(dep.angular.flat_module_metadata)
flat_module_out_file = dep.angular.flat_module_metadata.flat_module_out_file + ".js"
# For dependencies which are built using the "ng_module" with flat module bundles
# enabled, we determine the module name, the flat module index file, the metadata
# file and the typings entry point from the flat module metadata which is set by
# the "ng_module" rule.
ng_module_metadata = dep.angular.flat_module_metadata
module_name = ng_module_metadata.module_name
index_file = ng_module_metadata.flat_module_out_file + ".js"
typings_path = ng_module_metadata.typings_file.path
metadata_file = ng_module_metadata.metadata_file
guessed_paths = False
_debug(
ctx.var,
"entry-point %s is built using a flat module bundle." % dep,
"using %s as main file of the entry-point" % index_file,
)
else:
# In case the dependency is built through the "ts_library" rule, or the "ng_module"
# rule does not generate a flat module bundle, we determine the index file and
# typings entry-point through the most reasonable defaults (i.e. "package/index").
output_dir = "/".join([
p
for p in [
ctx.bin_dir.path,
ctx.label.package,
entry_point,
]
if p
])
# fallback to a reasonable default
flat_module_out_file = "index.js"
index_file = "index.js"
typings_path = "%s/index.d.ts" % output_dir
metadata_file = None
guessed_paths = True
_debug(
ctx.var,
"entry-point %s does not have flat module metadata." % dep,
"guessing %s as main file of the entry-point" % index_file,
)
# Store the collected entry point in a list of all entry-points. This
# can be later passed to the packager as a manifest.
collected_entry_points.append(struct(
module_name = module_name,
typings_path = typings_path,
metadata_file = metadata_file,
guessed_paths = guessed_paths,
))
if hasattr(dep, "dts_bundles"):
bundled_type_definitions += dep.dts_bundles
@ -258,8 +311,8 @@ def _ng_package_impl(ctx):
).to_list()
if len(type_definitions) > 0 and len(bundled_type_definitions) > 0:
# bundle_dts needs to be enabled/disabled for all ng module packages.
fail("Expected all or none of the 'ng_module' dependencies to have 'bundle_dts' enabled.")
# bundle_dts needs to be enabled/disabled for all entry points.
fail("Expected all or none of the entry points to have 'bundle_dts' enabled.")
es2015_entry_point = "/".join([p for p in [
ctx.bin_dir.path,
@ -267,13 +320,13 @@ def _ng_package_impl(ctx):
_esm2015_root_dir(ctx),
ctx.label.package,
entry_point,
flat_module_out_file,
index_file,
] if p])
es5_entry_point = "/".join([p for p in [
ctx.label.package,
entry_point,
flat_module_out_file,
index_file,
] if p])
if entry_point:
@ -314,8 +367,8 @@ def _ng_package_impl(ctx):
es5_entry_point,
esm5_rollup_inputs,
umd_output,
module_name = module_name,
format = "umd",
package_name = package_name,
include_tslib = True,
),
)
@ -349,12 +402,17 @@ def _ng_package_impl(ctx):
# Marshal the metadata into a JSON string so we can parse the data structure
# in the TypeScript program easily.
metadata_arg = {}
for m in flat_module_metadata:
packager_inputs.extend([m.metadata_file])
for m in collected_entry_points:
if m.metadata_file:
packager_inputs.extend([m.metadata_file])
metadata_arg[m.module_name] = {
"index": m.typings_file.path.replace(".d.ts", ".js"),
"typings": m.typings_file.path,
"metadata": m.metadata_file.path,
"index": m.typings_path.replace(".d.ts", ".js"),
"typings": m.typings_path,
# Metadata can be undefined if entry point is built with "ts_library".
"metadata": m.metadata_file.path if m.metadata_file else "",
# If the paths for that entry-point were guessed (e.g. "ts_library" rule or
# "ng_module" without flat module bundle), we pass this information to the packager.
"guessedPaths": "true" if m.guessed_paths else "",
}
packager_args.add(str(metadata_arg))

View File

@ -178,9 +178,14 @@ function main(args: string[]): number {
moduleFiles['esm5_index'] = path.join(binDir, 'esm5', relative);
moduleFiles['esm2015_index'] = path.join(binDir, 'esm2015', relative);
// Metadata file is optional as entry-points can be also built
// with the "ts_library" rule.
const metadataFile = moduleFiles['metadata'];
const typingsOutFile = moduleFiles['typings'];
if (!metadataFile) {
return;
}
const typingsOutFile = moduleFiles['typings'];
// We only support all modules within a package to be dts bundled
// ie: if @angular/common/http has flat dts, so should @angular/common
if (dtsBundles.length) {
@ -220,7 +225,7 @@ function main(args: string[]): number {
// Modify package.json files as necessary for publishing
if (path.basename(src) === 'package.json') {
const packageJson = JSON.parse(content);
content = amendPackageJson(src, packageJson);
content = amendPackageJson(src, packageJson, false);
const packageName = packageJson['name'];
packagesWithExistingPackageJson.add(packageName);
@ -240,8 +245,13 @@ function main(args: string[]): number {
const entryPointName = entryPointPackageName.substr(rootPackageName.length + 1);
if (!entryPointName) return;
createMetadataReexportFile(
entryPointName, modulesManifest[entryPointPackageName]['metadata'], entryPointPackageName);
const metadataFilePath = modulesManifest[entryPointPackageName]['metadata'];
if (metadataFilePath) {
createMetadataReexportFile(
entryPointName, modulesManifest[entryPointPackageName]['metadata'],
entryPointPackageName);
}
createTypingsReexportFile(
entryPointName, licenseBanner, modulesManifest[entryPointPackageName]['typings']);
@ -291,11 +301,19 @@ function main(args: string[]): number {
*
* @param packageJson The path to the package.json file.
* @param parsedPackage Parsed package.json content
* @param isGeneratedPackageJson Whether the passed package.json has been generated.
*/
function amendPackageJson(packageJson: string, parsedPackage: {[key: string]: string}) {
function amendPackageJson(
packageJson: string, parsedPackage: {[key: string]: string},
isGeneratedPackageJson: boolean) {
const packageName = parsedPackage['name'];
const moduleFiles = modulesManifest[packageName];
if (!moduleFiles) {
const moduleData = modulesManifest[packageName];
// We don't want to modify the "package.json" if we guessed the entry-point
// paths and there is a custom "package.json" for that package already. Module
// data will be only undefined if the package name comes from a non-generated
// "package.json". In that case we want to leave the file untouched as well.
if (!moduleData || moduleData.guessedPaths && !isGeneratedPackageJson) {
// Ideally we should throw here, as we got an entry point that doesn't
// have flat module metadata / bundle index, so it may have been an
// ng_module that's missing a module_name attribute.
@ -316,9 +334,9 @@ function main(args: string[]): number {
parsedPackage['fesm5'] = getBundleName(packageName, 'fesm5');
parsedPackage['fesm2015'] = getBundleName(packageName, 'fesm2015');
parsedPackage['esm5'] = srcDirRelative(packageJson, moduleFiles['esm5_index']);
parsedPackage['esm2015'] = srcDirRelative(packageJson, moduleFiles['esm2015_index']);
parsedPackage['typings'] = srcDirRelative(packageJson, moduleFiles['typings']);
parsedPackage['esm5'] = srcDirRelative(packageJson, moduleData['esm5_index']);
parsedPackage['esm2015'] = srcDirRelative(packageJson, moduleData['esm2015_index']);
parsedPackage['typings'] = srcDirRelative(packageJson, moduleData['typings']);
// For now, we point the primary entry points at the fesm files, because of Webpack
// performance issues with a large number of individual files.
@ -382,7 +400,7 @@ export * from '${srcDirRelative(inputPath, typingsFile.replace(/\.d\.tsx?$/, '')
*/
function createEntryPointPackageJson(dir: string, entryPointPackageName: string) {
const pkgJson = path.join(srcDir, dir, 'package.json');
const content = amendPackageJson(pkgJson, {name: entryPointPackageName});
const content = amendPackageJson(pkgJson, {name: entryPointPackageName}, true);
writeFileFromInputPath(pkgJson, content);
}
@ -444,4 +462,4 @@ export function newArray<T>(size: number, value?: T): T[] {
list.push(value !);
}
return list;
}
}