Skip to content

Commit

Permalink
feat(bazel): support ts_library targets as entry-points for ng_package
Browse files Browse the repository at this point in the history
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`.
  • Loading branch information
devversion committed Sep 11, 2019
1 parent 43bbc40 commit 2928cdc
Show file tree
Hide file tree
Showing 14 changed files with 1,180 additions and 33 deletions.
85 changes: 64 additions & 21 deletions packages/bazel/src/ng_package/ng_package.bzl
Expand Up @@ -98,7 +98,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()
Expand All @@ -107,9 +107,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.
Expand Down Expand Up @@ -228,22 +228,60 @@ 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
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

# 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
Expand All @@ -258,22 +296,22 @@ 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,
ctx.label.package,
_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:
Expand Down Expand Up @@ -314,8 +352,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,
),
)
Expand Down Expand Up @@ -349,12 +387,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))

Expand Down
42 changes: 30 additions & 12 deletions packages/bazel/src/ng_package/packager.ts
Expand Up @@ -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) {
Expand Down Expand Up @@ -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);
Expand All @@ -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']);

Expand Down Expand Up @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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);
}

Expand Down Expand Up @@ -444,4 +462,4 @@ export function newArray<T>(size: number, value?: T): T[] {
list.push(value !);
}
return list;
}
}
4 changes: 4 additions & 0 deletions packages/bazel/test/ng_package/BUILD.bazel
Expand Up @@ -62,7 +62,9 @@ jasmine_node_test(
srcs = [":example_spec_lib"],
data = [
"example_package.golden",
"example_with_ts_library_package.golden",
"//packages/bazel/test/ng_package/example:npm_package",
"//packages/bazel/test/ng_package/example-with-ts-library:npm_package",
],
# We don't want to run the example_package golden test with Ivy yet. Currently the golden
# file is based on non-ivy output and therefore won't work for ngc and Ivy at the same time.
Expand All @@ -76,8 +78,10 @@ nodejs_binary(
testonly = True,
data = [
"example_package.golden",
"example_with_ts_library_package.golden",
":example_spec_lib",
"//packages/bazel/test/ng_package/example:npm_package",
"//packages/bazel/test/ng_package/example-with-ts-library:npm_package",
"@npm//diff",
],
entry_point = ":example_package.spec.ts",
Expand Down
23 changes: 23 additions & 0 deletions packages/bazel/test/ng_package/example-with-ts-library/BUILD.bazel
@@ -0,0 +1,23 @@
load("//tools:defaults.bzl", "ng_package", "ts_library")

package(default_visibility = ["//packages/bazel/test:__subpackages__"])

ts_library(
name = "example",
srcs = glob(["*.ts"]),
module_name = "example",
deps = [],
)

ng_package(
name = "npm_package",
srcs = [
"package.json",
],
entry_point = ":index.ts",
deps = [
":example",
"//packages/bazel/test/ng_package/example-with-ts-library/portal",
"//packages/bazel/test/ng_package/example-with-ts-library/utils",
],
)
@@ -0,0 +1,9 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

export const VERSION = '0.0.0';
@@ -0,0 +1,13 @@
{
"name": "example",
"version": "0.0.0-PLACEHOLDER",
"main": "./bundles/example.umd.js",
"fesm5": "./fesm5/example.js",
"fesm2015": "./fesm2015/example.js",
"esm5": "./esm5/index.js",
"esm2015": "./esm2015/index.js",
"typings": "./index.d.ts",
"module": "./fesm5/example.js",
"es2015": "./fesm2015/example.js",
"schematics": "Custom property that should be preserved."
}
@@ -0,0 +1,14 @@
load("//tools:defaults.bzl", "ng_module")

package(default_visibility = ["//packages/bazel/test:__subpackages__"])

ng_module(
name = "portal",
srcs = glob(["*.ts"]),
bundle_dts = False,
module_name = "example/portal",
deps = [
"//packages/core",
"@npm//@types",
],
)
@@ -0,0 +1,9 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

export * from './portal-module';
@@ -0,0 +1,15 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

import {NgModule} from '@angular/core';

@NgModule({})
export class PortalModule {
}

export const a = 1;
@@ -0,0 +1,9 @@
load("//tools:defaults.bzl", "ts_library")

package(default_visibility = ["//packages/bazel/test:__subpackages__"])

ts_library(
name = "utils",
srcs = glob(["*.ts"]),
module_name = "example/utils",
)
@@ -0,0 +1,9 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

export * from './testing';
@@ -0,0 +1,11 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/

export function dispatchFakeEvent(el: HTMLElement, ev: Event) {
el.dispatchEvent(ev);
}

0 comments on commit 2928cdc

Please sign in to comment.