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 Feb 28, 2018
1 parent 13ab91e commit 23a7543
Show file tree
Hide file tree
Showing 30 changed files with 1,323 additions and 108 deletions.
58 changes: 39 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 @@ -296,10 +303,12 @@ def ng_module_impl(ctx, ts_compile_actions):
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 +329,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 +368,32 @@ NG_MODULE_ATTRIBUTES = {
"_supports_workers": attr.bool(default = True),
}

NG_MODULE_RULE_ATTRS = {
"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 = dict(dict(COMMON_ATTRIBUTES, **NG_MODULE_ATTRIBUTES), **NG_MODULE_RULE_ATTRS),
outputs = COMMON_OUTPUTS,
)

ivy_ng_module = rule(
implementation = _ivy_module_impl,
attrs = dict(dict(COMMON_ATTRIBUTES, **NG_MODULE_ATTRIBUTES), **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,32 @@
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,79 @@
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');
});
});
});
53 changes: 41 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,23 +312,49 @@ export class LowerMetadataTransform implements RequestsMap, MetadataTransformer
return false;
};

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);
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) && isLowerable(node)) {
return replaceNode(node);
}
return value;
};
}
}


function createExportTableFor(sourceFile: ts.SourceFile): Set<string> {
const exportTable = new Set<string>();
// Lazily collect all the exports from the source file
Expand Down

0 comments on commit 23a7543

Please sign in to comment.