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 1, 2018
1 parent c82cef8 commit 3864720
Show file tree
Hide file tree
Showing 32 changed files with 1,276 additions and 136 deletions.
2 changes: 1 addition & 1 deletion integration/_payload-limits.json
Expand Up @@ -3,7 +3,7 @@
"master": {
"uncompressed": {
"inline": 1447,
"main": 155112,
"main": 157654,
"polyfills": 59179
}
}
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 @@ -91,6 +97,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 @@ -281,7 +288,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 @@ -290,16 +297,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 @@ -320,6 +330,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 @@ -356,24 +369,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.
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,87 @@
/**
* @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, forwardRef} from '@angular/core';
import {AOT_TOKEN, AotModule, AotService} from 'app_built/src/module';

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

beforeEach(() => { injector = Injector.create({definitions: [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()
class Service {
}

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

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

it('works', () => { Injector.create({definitions: [JitAppModule]}); });

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

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

expect(() => Injector.create({
definitions: [AModule]
})).toThrowError('Circular dependency: module 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 = Injector.create({
definitions: [CModule],
});
expect(injector.get(TOKEN)).toEqual('provided from B');
});
});
});
51 changes: 39 additions & 12 deletions packages/compiler-cli/src/transformers/lower_expressions.ts
Expand Up @@ -248,11 +248,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 @@ -309,17 +312,41 @@ export class LowerMetadataTransform implements RequestsMap, MetadataTransformer
return false;
};

const hasLowerableParentCache = new Map<ts.Node, boolean>();
let hasLowerableParent: (node: ts.Node | undefined) => boolean;

const isLowerable = (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) &&
shouldLower(node)) {
lowerable = true;
}
if (isLiteralFieldNamed(node, this.lowerableFieldNames) && shouldLower(node) &&
!isExportedSymbol(node) && !isExportedPropertyAccess(node)) {
lowerable = true;
}
lowerable = lowerable && !hasLowerableParent(node);
return lowerable;
};

hasLowerableParent = (node: ts.Node | undefined): boolean => {
if (node === undefined) {
return false;
}
if (!hasLowerableParentCache.has(node)) {
hasLowerableParentCache.set(
node, isLowerable(node.parent) || hasLowerableParent(node.parent));
}
return hasLowerableParentCache.get(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 3864720

Please sign in to comment.