Skip to content

Commit

Permalink
feat(ivy): @NgModule -> ngInjectorDef compilation
Browse files Browse the repository at this point in the history
This adds compilation of @NgModule providers and imports into
ngInjectorDef statements in generated code. All @NgModule annotations
will be compiled and the @NgModule decorators removed from the
resultant js output.

All @Injectables will also be compiled in Ivy mode, and the decorator
removed.
  • Loading branch information
alxhub committed Mar 15, 2018
1 parent c09bd67 commit 2b653cc
Show file tree
Hide file tree
Showing 43 changed files with 1,974 additions and 147 deletions.
4 changes: 2 additions & 2 deletions integration/_payload-limits.json
Expand Up @@ -3,15 +3,15 @@
"master": {
"uncompressed": {
"inline": 1447,
"main": 155112,
"main": 157654,
"polyfills": 59179
}
}
},
"hello_world__closure": {
"master": {
"uncompressed": {
"bundle": 105779
"bundle": 106550
}
}
},
Expand Down
62 changes: 43 additions & 19 deletions packages/bazel/src/ng_module.bzl
Expand Up @@ -79,7 +79,13 @@ def _expected_outs(ctx):
i18n_messages = i18n_messages_files,
)

def _ivy_tsconfig(ctx, files, srcs, **kwargs):
return _ngc_tsconfig_helper(ctx, files, srcs, True, **kwargs)

def _ngc_tsconfig(ctx, files, srcs, **kwargs):
return _ngc_tsconfig_helper(ctx, files, srcs, False, **kwargs)

def _ngc_tsconfig_helper(ctx, files, srcs, enable_ivy, **kwargs):
outs = _expected_outs(ctx)
if "devmode_manifest" in kwargs:
expected_outs = outs.devmode_js + outs.declarations + outs.summaries
Expand All @@ -92,6 +98,7 @@ def _ngc_tsconfig(ctx, files, srcs, **kwargs):
"generateCodeForLibraries": False,
"allowEmptyCodegenFiles": True,
"enableSummariesForJit": True,
"enableIvy": enable_ivy,
"fullTemplateTypeCheck": ctx.attr.type_check,
# FIXME: wrong place to de-dupe
"expectedOut": depset([o.path for o in expected_outs]).to_list()
Expand Down Expand Up @@ -282,7 +289,7 @@ def _write_bundle_index(ctx):
)
return outputs

def ng_module_impl(ctx, ts_compile_actions):
def ng_module_impl(ctx, ts_compile_actions, ivy = False):
"""Implementation function for the ng_module rule.
This is exposed so that google3 can have its own entry point that re-uses this
Expand All @@ -291,16 +298,19 @@ def ng_module_impl(ctx, ts_compile_actions):
Args:
ctx: the skylark rule context
ts_compile_actions: generates all the actions to run an ngc compilation
ivy: if True, run the compiler in Ivy mode (internal only)
Returns:
the result of the ng_module rule as a dict, suitable for
conversion by ts_providers_dict_to_struct
"""

tsconfig = _ngc_tsconfig if not ivy else _ivy_tsconfig

providers = ts_compile_actions(
ctx, is_library=True, compile_action=_prodmode_compile_action,
devmode_compile_action=_devmode_compile_action,
tsc_wrapped_tsconfig=_ngc_tsconfig,
tsc_wrapped_tsconfig=tsconfig,
outputs = _ts_expected_outs)

outs = _expected_outs(ctx)
Expand All @@ -324,6 +334,9 @@ def ng_module_impl(ctx, ts_compile_actions):
def _ng_module_impl(ctx):
return ts_providers_dict_to_struct(ng_module_impl(ctx, compile_ts))

def _ivy_module_impl(ctx):
return ts_providers_dict_to_struct(ng_module_impl(ctx, compile_ts, True))

NG_MODULE_ATTRIBUTES = {
"srcs": attr.label_list(allow_files = [".ts"]),

Expand Down Expand Up @@ -362,24 +375,35 @@ NG_MODULE_ATTRIBUTES = {
"_supports_workers": attr.bool(default = True),
}

NG_MODULE_RULE_ATTRS = dict(dict(COMMON_ATTRIBUTES, **NG_MODULE_ATTRIBUTES), **{
"tsconfig": attr.label(allow_files = True, single_file = True),

# @// is special syntax for the "main" repository
# The default assumes the user specified a target "node_modules" in their
# root BUILD file.
"node_modules": attr.label(
default = Label("@//:node_modules")
),

"entry_point": attr.string(),

"_index_bundler": attr.label(
executable = True,
cfg = "host",
default = Label("//packages/bazel/src:index_bundler")),
})

ng_module = rule(
implementation = _ng_module_impl,
attrs = dict(dict(COMMON_ATTRIBUTES, **NG_MODULE_ATTRIBUTES), **{
"tsconfig": attr.label(allow_files = True, single_file = True),

# @// is special syntax for the "main" repository
# The default assumes the user specified a target "node_modules" in their
# root BUILD file.
"node_modules": attr.label(
default = Label("@//:node_modules")
),

"entry_point": attr.string(),

"_index_bundler": attr.label(
executable = True,
cfg = "host",
default = Label("//packages/bazel/src:index_bundler")),
}),
attrs = NG_MODULE_RULE_ATTRS,
outputs = COMMON_OUTPUTS,
)

# TODO(alxhub): this rule exists to allow early testing of the Ivy compiler within angular/angular,
# and should not be made public. When ng_module() supports Ivy-mode outputs, this rule should be
# removed and its usages refactored to use ng_module() directly.
internal_ivy_ng_module = rule(
implementation = _ivy_module_impl,
attrs = NG_MODULE_RULE_ATTRS,
outputs = COMMON_OUTPUTS,
)
@@ -0,0 +1,18 @@
package(default_visibility = ["//visibility:public"])

load("//tools:defaults.bzl", "ivy_ng_module", "ts_library")
load("//packages/bazel/src:ng_rollup_bundle.bzl", "ng_rollup_bundle")

ivy_ng_module(
name = "app",
srcs = glob(
[
"src/**/*.ts",
],
),
module_name = "app_built",
deps = [
"//packages/core",
"@rxjs",
],
)
@@ -0,0 +1,40 @@
/**
* @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 {Injectable, InjectionToken, NgModule} from '@angular/core';

export const AOT_TOKEN = new InjectionToken<string>('TOKEN');

@Injectable()
export class AotService {
}

@NgModule({
providers: [AotService],
})
export class AotServiceModule {
}

@NgModule({
providers: [{provide: AOT_TOKEN, useValue: 'imports'}],
})
export class AotImportedModule {
}

@NgModule({
providers: [{provide: AOT_TOKEN, useValue: 'exports'}],
})
export class AotExportedModule {
}

@NgModule({
imports: [AotServiceModule, AotImportedModule],
exports: [AotExportedModule],
})
export class AotModule {
}
@@ -0,0 +1,27 @@
package(default_visibility = ["//visibility:public"])

load("//tools:defaults.bzl", "ts_library")
load("@build_bazel_rules_nodejs//:defs.bzl", "jasmine_node_test")

ts_library(
name = "test_lib",
testonly = 1,
srcs = glob(
[
"**/*.ts",
],
),
deps = [
"//packages/compiler-cli/integrationtest/bazel/injector_def/ivy_build/app",
"//packages/core",
],
)

jasmine_node_test(
name = "test",
bootstrap = ["angular/tools/testing/init_node_spec.js"],
deps = [
":test_lib",
"//tools/testing:node",
],
)
@@ -0,0 +1,84 @@
/**
* @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 {Injectable, InjectionToken, Injector, NgModule, createInjector, forwardRef} from '@angular/core';
import {AOT_TOKEN, AotModule, AotService} from 'app_built/src/module';

describe('Ivy NgModule', () => {
describe('AOT', () => {
let injector: Injector;

beforeEach(() => { injector = createInjector(AotModule); });
it('works', () => { expect(injector.get(AotService) instanceof AotService).toBeTruthy(); });

it('merges imports and exports', () => { expect(injector.get(AOT_TOKEN)).toEqual('exports'); });
});



describe('JIT', () => {
@Injectable({providedIn: null})
class Service {
}

@NgModule({
providers: [Service],
})
class JitModule {
}

@NgModule({
imports: [JitModule],
})
class JitAppModule {
}

it('works', () => { createInjector(JitAppModule); });

it('throws an error on circular module dependencies', () => {
@NgModule({
imports: [forwardRef(() => BModule)],
})
class AModule {
}

@NgModule({
imports: [AModule],
})
class BModule {
}

expect(() => createInjector(AModule))
.toThrowError('Circular dependency: type AModule ends up importing itself.');
});

it('merges imports and exports', () => {
const TOKEN = new InjectionToken<string>('TOKEN');
@NgModule({
providers: [{provide: TOKEN, useValue: 'provided from A'}],
})
class AModule {
}
@NgModule({
providers: [{provide: TOKEN, useValue: 'provided from B'}],
})
class BModule {
}

@NgModule({
imports: [AModule],
exports: [BModule],
})
class CModule {
}

const injector = createInjector(CModule);
expect(injector.get(TOKEN)).toEqual('provided from B');
});
});
});
60 changes: 46 additions & 14 deletions packages/compiler-cli/src/transformers/lower_expressions.ts
Expand Up @@ -208,7 +208,7 @@ interface MetadataAndLoweringRequests {
requests: RequestLocationMap;
}

function shouldLower(node: ts.Node | undefined): boolean {
function isEligibleForLowering(node: ts.Node | undefined): boolean {
if (node) {
switch (node.kind) {
case ts.SyntaxKind.SourceFile:
Expand All @@ -226,7 +226,7 @@ function shouldLower(node: ts.Node | undefined): boolean {
// Avoid lowering expressions already in an exported variable declaration
return (ts.getCombinedModifierFlags(node) & ts.ModifierFlags.Export) == 0;
}
return shouldLower(node.parent);
return isEligibleForLowering(node.parent);
}
return true;
}
Expand All @@ -251,11 +251,14 @@ function isLiteralFieldNamed(node: ts.Node, names: Set<string>): boolean {
return false;
}

const LOWERABLE_FIELD_NAMES = new Set(['useValue', 'useFactory', 'data']);

export class LowerMetadataTransform implements RequestsMap, MetadataTransformer {
private cache: MetadataCache;
private requests = new Map<string, RequestLocationMap>();
private lowerableFieldNames: Set<string>;

constructor(lowerableFieldNames: string[]) {
this.lowerableFieldNames = new Set<string>(lowerableFieldNames);
}

// RequestMap
getRequests(sourceFile: ts.SourceFile): RequestLocationMap {
Expand Down Expand Up @@ -312,17 +315,46 @@ export class LowerMetadataTransform implements RequestsMap, MetadataTransformer
return false;
};

const hasLowerableParentCache = new Map<ts.Node, boolean>();

const shouldBeLowered = (node: ts.Node | undefined): boolean => {
if (node === undefined) {
return false;
}
let lowerable: boolean = false;
if ((node.kind === ts.SyntaxKind.ArrowFunction ||
node.kind === ts.SyntaxKind.FunctionExpression) &&
isEligibleForLowering(node)) {
lowerable = true;
} else if (
isLiteralFieldNamed(node, this.lowerableFieldNames) && isEligibleForLowering(node) &&
!isExportedSymbol(node) && !isExportedPropertyAccess(node)) {
lowerable = true;
}
return lowerable;
};

const hasLowerableParent = (node: ts.Node | undefined): boolean => {
if (node === undefined) {
return false;
}
if (!hasLowerableParentCache.has(node)) {
hasLowerableParentCache.set(
node, shouldBeLowered(node.parent) || hasLowerableParent(node.parent));
}
return hasLowerableParentCache.get(node) !;
};

const isLowerable = (node: ts.Node | undefined): boolean => {
if (node === undefined) {
return false;
}
return shouldBeLowered(node) && !hasLowerableParent(node);
};

return (value: MetadataValue, node: ts.Node): MetadataValue => {
if (!isPrimitive(value) && !isRewritten(value)) {
if ((node.kind === ts.SyntaxKind.ArrowFunction ||
node.kind === ts.SyntaxKind.FunctionExpression) &&
shouldLower(node)) {
return replaceNode(node);
}
if (isLiteralFieldNamed(node, LOWERABLE_FIELD_NAMES) && shouldLower(node) &&
!isExportedSymbol(node) && !isExportedPropertyAccess(node)) {
return replaceNode(node);
}
if (!isPrimitive(value) && !isRewritten(value) && isLowerable(node)) {
return replaceNode(node);
}
return value;
};
Expand Down

0 comments on commit 2b653cc

Please sign in to comment.