Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(ivy): @NgModule -> ngInjectorDef compilation #22458

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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 @@ -283,7 +290,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 @@ -292,16 +299,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 @@ -325,6 +335,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 @@ -363,24 +376,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