Compare commits

..

59 Commits

Author SHA1 Message Date
ca7ee794bf release: cut the v10.0.2 release 2020-06-30 11:40:40 -07:00
f9f2ba6faf docs: add Sonu Kapoor to the collaborator list (#37777)
After 6 months of continuous contributions, Sonu Kapoor did finally make
it into the collaborator list.

PR Close #37777
2020-06-30 10:47:55 -07:00
aea1d211d4 docs: update /config/app-package-json redirect (#37774)
With this change we change the redirect for `/config/app-package-json` from `https://webpack.js.org/configuration/optimization/#optimizationsideeffects` to `https://angular.io/guide/strict-mode#non-local-side-effects-in-applications`

The latter page has more details.

PR Close #37774
2020-06-30 10:45:53 -07:00
57a518a36d perf(compiler-cli): fix memory leak in retained incremental state (#37835)
Incremental compilation allows for the output state of one compilation to be
reused as input to the next compilation. This involves retaining references
to instances from prior compilations, which must be done carefully to avoid
memory leaks.

This commit fixes such a leak with a complicated retention chain:

* `TrackedIncrementalBuildStrategy` unnecessarily hangs on to the previous
  `IncrementalDriver` (state of the previous compilation) once the current
  compilation completes.

  In general this is unnecessary, but should be safe as long as the chain
  only goes back one level - if the `IncrementalDriver` doesn't retain any
  previous `TrackedIncrementalBuildStrategy` instances. However, this does
  happen:

* `NgCompiler` indirectly causes retention of previous `NgCompiler`
  instances (and thus previous `TrackedIncrementalBuildStrategy` instances)
  through accidental capture of the `this` context in a closure created in
  its constructor. This closure is wrapped in a `ts.ModuleResolutionCache`
  used to create a `ModuleResolver` class, which is passed to the program's
  `TraitCompiler` on construction.

* The `IncrementalDriver` retains a reference to the `TraitCompiler` of the
  previous compilation, completing the reference chain.

The final retention chain thus looks like:

* `TrackedIncrementalBuildStrategy` of current program
* `.previous`: `IncrementalDriver` of previous program
* `.lastGood.traitCompiler`: `TraitCompiler`
* `.handlers[..].moduleResolver.moduleResolutionCache`: cache
* (via `getCanonicalFileName` closure): `NgCompiler`
* `.incrementalStrategy`: `TrackedIncrementalBuildStrategy` of previous
  program.

The closure link is the "real" leak here. `NgCompiler` is creating a closure
for `getCanonicalFileName`, delegating to its
`this.adapter.getCanonicalFileName`, for the purposes of creating a
`ts.ModuleResolutionCache`. The fact that the closure references
`NgCompiler` thus eventually causes previous `NgCompiler` iterations to be
retained. This is also potentially problematic due to the shared nature of
`ts.ModuleResolutionCache`, which is potentially retained across multiple
compilations intentionally.

This commit fixes the first two links in the retention chain: the build
strategy is patched to not retain a `previous` pointer, and the `NgCompiler`
is patched to not create a closure in the first place, but instead pass a
bound function. This ensures that the `NgCompiler` does not retain previous
instances of itself in the first place, even if the build strategy does
end up retaining the previous incremental state unnecessarily.

The third link (`IncrementalDriver` unnecessarily retaining the whole
`TraitCompiler`) is not addressed in this commit as it's a more
architectural problem that will require some refactoring. However, the leak
potential of this retention is eliminated thanks to fixing the first two
issues.

PR Close #37835
2020-06-29 16:34:52 -07:00
29b83189b0 build(docs-infra): update to latest dgeni-packages (#37793)
This update of dgeni-packages to 0.28.4 fixes the
rendering of type initializers for classes and interfaces.

Closes #37694

PR Close #37793
2020-06-29 15:01:16 -07:00
1d3df7885d docs: correct the spelling mistake in observables error handling code (#36437)
This commit fixes a spelling error in the word error in the
observables.md guide. It is currently
spelled errror  and the mistake is not intentional.

PR Close #36437
2020-06-29 15:00:39 -07:00
fd06ffa2af docs: change definition of providedIn any (#35292)
change in the definition of providedIn:any any instance creates a singleton instance
for each lazy loaded module and one instance for eager loaded module

PR Close #35292
2020-06-29 15:00:01 -07:00
36a1622dd1 docs: correct left nav to remove duplicated page links (#37833)
The major sections Angular Libraries, Schematics, and CLI Builders appear twice, in their old location under Techniques, and in the new correct location under Extending Angular.

PR Close #37833
2020-06-29 14:57:37 -07:00
7a91b23cb5 fix(core): fake_async_fallback should have the same logic with fake-async (#37680)
PR https://github.com/angular/angular/pull/37523 failed when trying to use `rxjs delay` operator
inside `fakeAsync`, and the reasons are:

1. we need to import `rxjs-fake-async` patch to make the integration work.
2. since in `angular` repo, the bazel target `/tools/testing:node` not using `zone-testing` bundle,
instead it load `zone-spec` packages seperately, so it causes one issue which is the `zone.js/testing/fake-async`
package is not loaded, we do have a fallback logic under `packages/core/testing` calles `fake_async_fallback`,
but the logic is out of date with `fake-async` under `zone.js` package.

So this PR, I updated the content of `fake_async_fallback` to make it consistent with
`fake-async`. And I will make another PR to try to remove the `fallback` logic.

PR Close #37680
2020-06-29 12:22:52 -07:00
4b90b6a226 fix(ngcc): prevent including JavaScript sources outside of the package (#37596)
When ngcc creates an entry-point program, the `allowJs` option is enabled
in order to operate on the JavaScript source files of the entry-point.
A side-effect of this approach is that external modules that don't ship
declaration files will also have their JavaScript source files loaded
into the program, as the `allowJs` flag allows for them to be imported.
This may pose an issue in certain edge cases, where ngcc would inadvertently
operate on these external modules. This can introduce all sorts of undesirable
behavior and incompatibilities, e.g. the reflection host that is selected for
the entry-point's format could be incompatible with that of the external
module's JavaScript bundles.

To avoid these kinds of issues, module resolution that would resolve to
a JavaScript file located outside of the package will instead be rejected,
as if the file would not exist. This would have been the behavior when
`allowJs` is set to false, which is the case in typical Angular compilations.

Fixes #37508

PR Close #37596
2020-06-29 12:21:23 -07:00
b13daa4cdf refactor(ngcc): let isWithinPackage operate on paths instead of source files (#37596)
Changes `isWithinPackage` to take an `AbsoluteFsPath` instead of `ts.SourceFile`,
to allow for an upcoming change to use it when no `ts.SourceFile` is available,
but just a path.

PR Close #37596
2020-06-29 12:21:23 -07:00
0c6f026828 docs: Changing typo Stacblitz into Stackblitz in the Tour of Hereos tutorial docs page (#37794)
Changing the typo of Stacblitz into Stackblitz in the tour of hereos tutorial docs page since that is the actual name of the service

PR Close #37794
2020-06-29 12:17:41 -07:00
a2520bd267 docs: remove first person from 2 sentences (#37768)
This commit removes two instances of the first person in the
Dependency injection providers documentation.

PR Close #37768
2020-06-29 12:17:04 -07:00
b928a209a4 docs: add Amadou Sall to GDE page (#36509)
This commit adds Amadou Sall to the Angular GDE page along with a
biography, his role at Air France, and a photograph.

PR Close #36509
2020-06-29 12:16:23 -07:00
89e16ed6a5 fix(elements): fire custom element output events during component initialization (#37570)
Previously, event listeners for component output events attached on an
Angular custom element before inserting it into the DOM (i.e. before
instantiating the underlying component) didn't fire for events emitted
during initialization lifecycle hooks, such as `ngAfterContentInit`,
`ngAfterViewInit`, `ngOnChanges` (initial call) and `ngOnInit`.
The reason was that `NgElementImpl` [subscribed to events][1] _after_
calling [ngElementStrategy#connect()][2], which is where the
[initial change detection][3] takes place (running the initialization
lifecycle hooks).

This commit fixes this by:
1. Ensuring `ComponentNgElementStrategy#events` is defined and available
   for subscribing to, even before instantiating the component.
2. Changing `NgElementImpl` to subscribe to `NgElementStrategy#events`
   (if available) before calling `NgElementStrategy#connect()` (which
   initializes the component instance) if available.
3. Falling back to the old behavior (subscribing to `events` after
   calling `connect()` for strategies that do not initialize `events`
   before their `connect()` is run).

NOTE:
By falling back to the old behavior when `NgElementStrategy#events` is
not initialized before calling `NgElementStrategy#connect()`, we avoid
breaking existing custom `NgElementStrategy` implementations (with
@remackgeek's [ElementZoneStrategy][4] being a commonly used example).

Jira issue: [FW-2010](https://angular-team.atlassian.net/browse/FW-2010)

[1]: c0143cb2ab/packages/elements/src/create-custom-element.ts (L167-L170)
[2]: c0143cb2ab/packages/elements/src/create-custom-element.ts (L164)
[3]: c0143cb2ab/packages/elements/src/component-factory-strategy.ts (L158)
[4]: f1b6699495/projects/elements-zone-strategy/src/lib/element-zone-strategy.ts

Fixes #36141

PR Close #37570
2020-06-29 10:33:40 -07:00
1a1f99af37 fix(ngcc): ensure lockfile is removed when analyzeFn fails (#37739)
Previously an error thrown in the `analyzeFn` would cause
the ngcc process to exit immediately without removing the
lockfile, and potentially before the unlocker process had been
successfully spawned resulting in the lockfile being orphaned
and left behind.

Now we catch these errors and remove the lockfile as needed.

PR Close #37739
2020-06-29 10:29:12 -07:00
df2cd37ed2 fix(core): error when invoking callbacks registered via ViewRef.onDestroy (#37543) (#37783)
Invoking a callback registered through `ViewRef.onDestroy` throws an error, because we weren't registering it correctly in the internal data structure. These changes also remove the `storeCleanupFn` function, because it was mostly identical to `storeCleanupWithContext` and was only used in one place.

Fixes #36213.

PR Close #37543

PR Close #37783
2020-06-29 10:27:39 -07:00
12a71bc6bc fix(core): determine required DOMParser feature availability (#36578) (#37783)
Verify that HTML parsing is supported in addition to DOMParser existence.
This maybe wasn't as important before when DOMParser was used just as a
fallback on Firefox, but now that DOMParser is the default choice, we need
to be more accurate.

PR Close #37783
2020-06-29 10:27:39 -07:00
7d270c235a refactor(core): split inert strategies to separate classes (#36578) (#37783)
The `inertDocument` member is only needed when using the InertDocument
strategy. By separating the DOMParser and InertDocument strategies into
separate classes, we can easily avoid creating the inert document
unnecessarily when using DOMParser.

PR Close #37783
2020-06-29 10:27:39 -07:00
b0b7248504 fix(core): do not trigger CSP alert/report in Firefox and Chrome (#36578) (#37783)
If [innerHTML] is used in a component and a Content-Security-Policy is set
that does not allow inline styles then Firefox and Chrome show the following
message:

> Content Security Policy: The page’s settings observed the loading of a
resource at self (“default-src”). A CSP report is being sent.

This message is caused because Angular is creating an inline style tag to
test for a browser bug that we use to decide what sanitization strategy to
use, which causes CSP violation errors if inline CSS is prohibited.

This test is no longer necessary, since the `DOMParser` is now safe to use
and the `style` based check is redundant.

In this fix, we default to using `DOMParser` if it is available and fall back
to `createHTMLDocument()` if needed. This is the approach used by DOMPurify
too.

The related unit tests in `html_sanitizer_spec.ts`, "should not allow
JavaScript execution when creating inert document" and "should not allow
JavaScript hidden in badly formed HTML to get through sanitization (Firefox
bug)", are left untouched to assert that the behavior hasn't changed in
those scenarios.

Fixes #25214.

PR Close #37783
2020-06-29 10:27:38 -07:00
78460c1848 test(core): update symbols used in the test app (#37785)
This commit updates the golden file that contains the set of symbols used in the test TODO app. The `storeCleanupFn` function was replaced by `storeCleanupWithContext` in 75b119eafc and this commit updates the golden file to reflect that.

PR Close #37785
2020-06-26 16:44:00 -07:00
75b119eafc fix(core): error when invoking callbacks registered via ViewRef.onDestroy (#37543)
Invoking a callback registered through `ViewRef.onDestroy` throws an error, because we weren't registering it correctly in the internal data structure. These changes also remove the `storeCleanupFn` function, because it was mostly identical to `storeCleanupWithContext` and was only used in one place.

Fixes #36213.

PR Close #37543
2020-06-26 15:02:43 -07:00
64b0ae93f7 fix(core): don't consider inherited NG_ELEMENT_ID during DI (#37574)
Special DI tokens like `ChangeDetectorRef` and `ElementRef` can provide a factory via `NG_ELEMENT_ID`. The problem is that we were reading it off the token as `token[NG_ELEMENT_ID]` which will go up the prototype chain if it couldn't be found on the current token, resulting in the private `ViewRef` API being exposed, because it extends `ChangeDetectorRef`.

These changes fix the issue by guarding the property access with `hasOwnProperty`.

Fixes #36235.

PR Close #37574
2020-06-26 15:01:21 -07:00
7c0b25f5a6 fix(language-service): incorrect autocomplete results on unknown symbol (#37518)
This commit fixes a bug whereby the language service would incorrectly
return HTML elements if autocomplete is requested for an unknown symbol.
This is because we walk through every possible scenario, and fallback to
element autocomplete if none of the scenarios match.

The fix here is to return results from interpolation if we know for sure
we are in a bound text. This means we will now return an empty results if
there is no suggestions.

This commit also refactors the code a little to make it easier to
understand.

PR Close #37518
2020-06-26 14:51:33 -07:00
07b5df3a19 release: cut the v10.0.1 release 2020-06-26 13:17:36 -07:00
e7023726f4 ci: exclude "docs" commit type from minBodyLength commit message validation (#37764)
docs commits are sometimes trivial (e.g. an obvious typo fix) and in such cases its very
akward to to write up 100 chars worth of text about why this typo fix is the best thing in the
world and why it is so important and crucial that we must know why we are fixing the typo
at all. After all most typos are not just typos. Or are they? We'll shall see...

PR Close #37764
2020-06-26 11:13:10 -07:00
a9ccd9254c feat(dev-infra): add support for minBodyLengthTypeExcludes to commit-message validation (#37764)
This feature will allow us to exclude certain commits from the 100 chars minBodyLength requirement for commit
messages which is hard to satisfy for commits that make trivial changes (e.g. fixing typos in docs or comments).

PR Close #37764
2020-06-26 11:13:10 -07:00
335f3271d2 refactor(core): throw more descriptive error message in case of invalid host element (#35916)
This commit replaces an assert with more descriptive error message that is thrown in case `<ng-template>` or `<ng-container>` is used as host element for a Component.

Resolves #35240.

PR Close #35916
2020-06-26 11:10:15 -07:00
7f93f7ef47 build: move shims_for_IE to third_party directory (#37624)
The shims_for_IE.js file contains vendor code that predates the third_party
directory. This file is currently used for internal karma testing setup. This
change corrects this by moving the shims_for_IE file to //third_part/

PR Close #37624
2020-06-26 11:09:02 -07:00
cf46a87fcd refactor(compiler-cli): Remove any cast for CompilerHost (#37079)
This commit removes the FIXME for casting CompilerHost to any since
google3 is now already on TS 3.8.

PR Close #37079
2020-06-26 11:08:18 -07:00
ad6680f602 fix(language-service): reinstate getExternalFiles() (#37750)
`getExternalFiles()` is an API that could optionally be provided by a tsserver plugin
to notify the server of any additional files that should belong to a particular project.

This API was removed in https://github.com/angular/angular/pull/34260 mainly
due to performance reasons.

However, with the introduction of "solution-style" tsconfig in typescript 3.9,
the Angular extension could no longer reliably detect the owning Project solely
based on the ancestor tsconfig.json. In order to support this use case, we have
to reinstate `getExternalFiles()`.

Fixes https://github.com/angular/vscode-ng-language-service/issues/824

PR Close #37750
2020-06-26 09:57:08 -07:00
5e287f67af docs: correct outdated dev instructions for public api golds (#37026)
This change updates the dev instructions to reflect the location and generation of public API golds, which changed in #35768.

PR Close #37026
2020-06-26 09:56:33 -07:00
ecfe6e0609 docs: add note about the month being zero-based in the Date constructor (#37770)
Because the month is zero based, it may confuse some users that '3'
is in fact 'April'. This comment should clear that up.

PR Close #37770
2020-06-26 09:55:19 -07:00
df9790dd11 fix(dev-infra): support running scripts from within a detached head (#37737)
Scripts provided in the `ng-dev` command might use local `git`
commands. For such scripts, we keep track of the branch that
has been checked out before the command has been invoked.

We do this so that we can later (upon command completion)
restore back to the original branch. We do not want to
leave the Git repository in a dirty state.

It looks like this logic currently only deals with branches
but does not work properly when a command is invoked from
a detached head. We can make it work by just checking out
the previous revision (if no branch is checked out).

PR Close #37737
2020-06-26 09:51:10 -07:00
67cfc4c9bc build: add wombot proxy for publish config for @angular/benchpress (#37752)
Adds the publishConfig registry value to the package.json of the
@angular/benchpress package to publish it via wombat rather than
through npm directly.

PR Close #37752
2020-06-25 17:08:19 -07:00
a68e623c80 docs(elements): fixed command that adds the package @angular/elements (#37681)
I was using schematics with the `--name` parameter instead of the `--project`, I did both ways before sending and my suspicion about outdated documentation was confirmed

PR Close #37681
2020-06-25 17:07:30 -07:00
9e3915ba48 docs: typo fixes for schematics-for-libraries.md (#37753)
Addresses small typos such as extra whitespaces.

This change was extracted from #29505.
This change was extracted from #29505.
This change was extracted from #29505.

PR Close #37753
2020-06-25 17:06:38 -07:00
ba2de61748 fix(docs-infra): fix deploy-to-firebase.sh for master and v10.0.x branches (#37762)
The deployment to aio is currently failing because #37721 introduced
"project" entry into the firebase.json which means that we now need to
select the deployment target before deploying to firebase.

This change fixes the issue and refactors the file to be easier to read.

I also added extra echo statements so that the CI logs are easier to
read in case we need to troubleshoot future issues.

PR Close #37762
2020-06-25 17:03:25 -07:00
a9a4edebe2 fix(docs-infra): fix typo in the deploy-to-firebase.sh script (#37754)
This typo caused the script to fail on Linux (interestingly it works fine on Mac).

This is a painful reminder that we should not write any more Bash scripts EVER. shelljs FTW! :-)

PR Close #37754
2020-06-25 15:21:25 -07:00
64f2ffa166 fix(core): cleanup DOM elements when root view is removed (#37600)
Currently when bootstrapped component is being removed using `ComponentRef.destroy` or `NgModuleRef.destroy` methods, DOM nodes may be retained in the DOM tree. This commit fixes that problem by always attaching host element of the internal root view to the component's host view node, so the cleanup can happen correctly.

Resolves #36449.

PR Close #37600
2020-06-25 14:34:36 -07:00
13020b9cc2 fix(migrations): do not incorrectly add todo for @Injectable or @Pipe (#37732)
As of v10, the `undecorated-classes-with-decorated-fields` migration
generally deals with undecorated classes using Angular features. We
intended to run this migation as part of v10 again as undecorated
classes with Angular features are no longer supported in planned v11.

The migration currently behaves incorrectly in some cases where an
`@Injectable` or `@Pipe` decorated classes uses the `ngOnDestroy`
lifecycle hook. We incorrectly add a TODO for those classes. This
commit fixes that.

Additionally, this change makes the migration more robust to
not migrate a class if it inherits from a component, pipe
injectable or non-abstract directive. We previously did not
need this as the undecorated-classes-with-di migration ran
before, but this is no longer the case.

Last, this commit fixes an issue where multiple TODO's could be
added. This happens when multiple Angular CLI build targets have
an overlap in source files. Multiple programs then capture the
same source file, causing the migration to detect an undecorated
class multiple times (i.e. adding a TODO twice).

Fixes #37726.

PR Close #37732
2020-06-25 14:22:09 -07:00
96b96fba0f perf(compiler-cli): fix regressions in incremental program reuse (#37690)
Commit 4213e8d5 introduced shim reference tagging into the compiler, and
changed how the `TypeCheckProgramHost` worked under the hood during the
creation of a template type-checking program. This work enabled a more
incremental flow for template type-checking, but unintentionally introduced
several regressions in performance, caused by poor incrementality during
`ts.Program` creation.

1. The `TypeCheckProgramHost` was made to rely on the `ts.CompilerHost` to
   retrieve instances of `ts.SourceFile`s from the original program. If the
   host does not return the original instance of such files, but instead
   creates new instances, this has two negative effects: it incurs
   additional parsing time, and it interferes with TypeScript's ability to
   reuse information about such files.

2. During the incremental creation of a `ts.Program`, TypeScript compares
   the `referencedFiles` of `ts.SourceFile` instances from the old program
   with those in the new program. If these arrays differ, TypeScript cannot
   fully reuse the old program. The implementation of reference tagging
   introduced in 4213e8d5 restores the original `referencedFiles` array
   after a `ts.Program` is created, which means that future incremental
   operations involving that program will always fail this comparison,
   effectively limiting the incrementality TypeScript can achieve.

Problem 1 exacerbates problem 2: if a new `ts.SourceFile` is created by the
host after shim generation has been disabled, it will have an untagged
`referencedFiles` array even if the original file's `referencedFiles` was
not restored, triggering problem 2 when creating the template type-checking
program.

To fix these issues, `referencedFiles` arrays are now restored on the old
`ts.Program` prior to the creation of a new incremental program. This allows
TypeScript to get the most out of reusing the old program's data.

Additionally, the `TypeCheckProgramHost` now uses the original `ts.Program`
to retrieve original instances of `ts.SourceFile`s where possible,
preventing issues when a host would otherwise return fresh instances.

Together, these fixes ensure that program reuse is as incremental as
possible, and tests have been added to verify this for certain scenarios.

An optimization was further added to prevent the creation of a type-checking
`ts.Program` in the first place if no type-checking is necessary.

PR Close #37690
2020-06-25 14:13:34 -07:00
2cbe53a9ba docs: Uses correct component in the MessageService (#37666)
This commit uses the correct component (`HeroesComponent`) in the.
`MessageService`. Previously, the `MessageService` was using
`HeroeService`.

Closes #37654

PR Close #37666
2020-06-25 13:49:00 -07:00
48755114e5 feat(docs-infra): update deploy-to-firebase.sh script to support v9 multisite setup (#37721)
v9.angular.io was used to pilot the firebase hosting multisites setup for angular.io.

The deployments so far have been done manually to control the deployment process.

This change, automates the deployment for v9.angular.io so that future deployments can be made from
the CI.

See https://angular-team.atlassian.net/browse/DEV-125 for more info.

In the process of updating the scripts I rediscovered a bug in the deploy-to-firebase.sh script that
incorrect compared two numbers as strings. This previously worked correctly because we were comparing
single digit numbers. With the release of v10, we now compare 9 > 10 which behaves differently for
strings and numbers. The bug was fixed by switching to an arithmetic comparison of the two variables.

This bug has been fixed on the master branch but not on the 9.1.x branch. I realized this during the
rebase, but found my version to be a bit cleaner, so I kept it.

PR Close #37721
2020-06-25 13:44:53 -07:00
a5d5f67be7 fix(http): avoid abort a request when fetch operation is completed (#37367)
`abort` method is calling, even if fetch operation is completed

Fixes https://github.com/angular/angular/issues/36537

PR Close #37367
2020-06-25 12:09:40 -07:00
dfb58c44a2 fix(forms): correct usage of selectedOptions (#37620)
Previously, `registerOnChange` used `hasOwnProperty` to identify if the
property is supported. However, this does not work as the `selectedOptions`
property is an inherited property. This commit fixes this by verifying
the property on the prototype instead.

Closes #37433

PR Close #37620
2020-06-25 12:08:01 -07:00
69948ce919 fix(router): add null support for RouterLink directive (#32616)
Value of "undefined" passed as segment in routerLink is stringified to string "undefined".
This change introduces the same behavior for value of "null".

PR Close #32616
2020-06-25 11:58:01 -07:00
3190ccf3b2 fix(router): fix error when calling ParamMap.get function (#31599)
fix this.params.hasOwnProperty is not a function in case of creating an object using Object.create()

PR Close #31599
2020-06-25 11:57:25 -07:00
a8ea8173aa fix(router): RouterLinkActive should run CD when setting isActive (#21411)
When using the routerLinkActive directive inside a component that is using ChangeDetectionStrategy.OnPush and lazy loaded module routes the routerLinkActive directive does not update after clicking a link to a lazy loaded route that has not already been loaded.

Also the OnPush nav component does not set routerLinkActive correctly when the default route loads, the non-OnPush nav component works fine.

regression caused by #15943
closes #19934

PR Close #21411
2020-06-25 11:56:26 -07:00
e13a49d1f0 feat(dev-infra): add a way to pass assets down to a benchmark application (#37695)
* add a param called ng_assets to the component_benchmark macro to allow static assets to be provided to the base angular app, not just through the ts_devserver

PR Close #37695
2020-06-25 11:51:52 -07:00
2f0b8f675a docs: Add support schedule for v10 (#37745)
This commit adds the support schedule for v10.
v10.0.0 was released on June 24, 2020.
Active support ends six months later, on Dec 24, 2020.
Long term support ends a year after that, on Dec 24, 2021.

PR Close #37745
2020-06-25 11:49:18 -07:00
c2aed033ba ci(compiler-cli): exempt compiler-cli .bazel files from dev-infra approval (#37558)
Previously, dev-infra approval (via PullApprove) was required for all
.bazel files in the monorepo, including those in packages/compiler-cli.

The compiler-cli package is a little special in this sense:
 * it's not shipped to NPM in the APF
 * it uses lots of internal subpackages to organize and test its code

As a result:
 * changes to compiler-cli BUILD.bazel files are not user visible and
   don't have larger implications for the packages published to NPM,
   unlike changes to other BUILD.bazel files in the repo
 * the requirement for dev-infra approval for BUILD.bazel changes is
   overly burdensome, because compiler-cli build files change more
   rapidly than those of other packages.

This commit exempts the compiler-cli's build files from the requirement
for dev-infra approval. It will be sufficient for such files to be
approved by the normal compiler reviewers.

PR Close #37558
2020-06-25 11:47:51 -07:00
0f8a780b0d docs: Replace $emdash; with an actual em dash (#37723)
fix documentation in the lifecycle hooks guide where $emdash; was not being replaced by an actual em dash (-)

PR Close #37723
2020-06-25 11:41:54 -07:00
c5bc2e77c8 fix(forms): change error message (#37643)
Error message mention that ngModel and ngModelChange will be removed in Angular v7 but right not now sure when it will be removed so changed it to a future version

PR Close #37643
2020-06-25 11:37:00 -07:00
079310dc7c test(docs-infra): add end to end tests for api reference (#37612)
Api search functionality only had unit tests @gkalpak suggested we should have some e2e tests too. Added some end to end tests.

Fixes #35170

PR Close #37612
2020-06-25 11:36:03 -07:00
0d2cdf6165 docs: add v9.angular.io to the angular.io version picker (#37719)
Now that v10 is out, it's time to add the v9.angular.io link to the version picker, so here we go...

PR Close #37719
2020-06-24 19:53:25 -07:00
436dde271f docs(changelog): fix v10 announcement url (#37722)
PR Close #37722
2020-06-24 19:50:45 -07:00
96891a076f release: sort the v10.0.0 release in CHANGELOG.md 2020-06-24 15:58:06 -07:00
9ce0067bdf release: consolidate the v10.0.0 release CHANGELOG.md 2020-06-24 13:55:21 -07:00
130 changed files with 2533 additions and 1299 deletions

View File

@ -4,6 +4,7 @@ import {MergeConfig} from '../dev-infra/pr/merge/config';
const commitMessage = {
'maxLength': 120,
'minBodyLength': 100,
'minBodyLengthExcludes': ['docs'],
'types': [
'build',
'ci',
@ -56,8 +57,6 @@ const format = {
// TODO: burn down format failures and remove aio and integration exceptions.
'!aio/**',
'!integration/**',
// TODO: remove this exclusion as part of IE deprecation.
'!shims_for_IE.js',
// Both third_party and .yarn are directories containing copied code which should
// not be modified.
'!third_party/**',

View File

@ -1036,7 +1036,7 @@ groups:
conditions:
- *can-be-global-approved
- >
contains_any_globs(files.exclude("CHANGELOG.md"), [
contains_any_globs(files.exclude("CHANGELOG.md").exclude("packages/compiler-cli/**/BUILD.bazel"), [
'*',
'.circleci/**',
'.devcontainer/**',

View File

@ -24,7 +24,7 @@ filegroup(
"//packages/zone.js/dist:zone-testing.js",
"//packages/zone.js/dist:task-tracking.js",
"//:test-events.js",
"//:shims_for_IE.js",
"//:third_party/shims_for_IE.js",
# Including systemjs because it defines `__eval`, which produces correct stack traces.
"@npm//:node_modules/systemjs/dist/system.src.js",
"@npm//:node_modules/reflect-metadata/Reflect.js",

File diff suppressed because it is too large Load Diff

View File

@ -6,5 +6,5 @@ import { Component } from '@angular/core';
templateUrl: './app.component.html'
})
export class AppComponent {
birthday = new Date(1988, 3, 15); // April 15, 1988
birthday = new Date(1988, 3, 15); // April 15, 1988 -- since month parameter is zero-based
}

View File

@ -8,5 +8,5 @@ import { Component } from '@angular/core';
// #enddocregion hero-birthday-template
})
export class HeroBirthdayComponent {
birthday = new Date(1988, 3, 15); // April 15, 1988
birthday = new Date(1988, 3, 15); // April 15, 1988 -- since month parameter is zero-based
}

View File

@ -12,7 +12,7 @@ import { Component } from '@angular/core';
})
// #docregion class
export class HeroBirthday2Component {
birthday = new Date(1988, 3, 15); // April 15, 1988
birthday = new Date(1988, 3, 15); // April 15, 1988 -- since month parameter is zero-based
toggle = true; // start with true == shortDate
get format() { return this.toggle ? 'shortDate' : 'fullDate'; }

View File

@ -33,7 +33,7 @@ export class HeroesComponent implements OnInit {
onSelect(hero: Hero): void {
this.selectedHero = hero;
this.messageService.add(`HeroService: Selected hero id=${hero.id}`);
this.messageService.add(`HeroesComponent: Selected hero id=${hero.id}`);
}
// #docregion getHeroes

View File

@ -197,11 +197,11 @@ Like `EvenBetterLogger`, `HeroService` needs to know if the user is authorized
That authorization can change during the course of a single application session,
as when you log in a different user.
Let's say you don't want to inject `UserService` directly into `HeroService`, because you don't want to complicate that service with security-sensitive information.
Imagine that you don't want to inject `UserService` directly into `HeroService`, because you don't want to complicate that service with security-sensitive information.
`HeroService` won't have direct access to the user information to decide
who is authorized and who isn't.
To resolve this, we give the `HeroService` constructor a boolean flag to control display of secret heroes.
To resolve this, give the `HeroService` constructor a boolean flag to control display of secret heroes.
<code-example path="dependency-injection/src/app/heroes/hero.service.ts" region="internals" header="src/app/heroes/hero.service.ts (excerpt)"></code-example>

View File

@ -119,7 +119,7 @@ The recently-developed [custom elements](https://developer.mozilla.org/en-US/doc
In browsers that support Custom Elements natively, the specification requires developers use ES2015 classes to define Custom Elements - developers can opt-in to this by setting the `target: "es2015"` property in their project's [TypeScript configuration file](/guide/typescript-configuration). As Custom Element and ES2015 support may not be available in all browsers, developers can instead choose to use a polyfill to support older browsers and ES5 code.
Use the [Angular CLI](cli) to automatically set up your project with the correct polyfill: `ng add @angular/elements --name=*your_project_name*`.
Use the [Angular CLI](cli) to automatically set up your project with the correct polyfill: `ng add @angular/elements --project=*your_project_name*`.
- For more information about polyfills, see [polyfill documentation](https://www.webcomponents.org/polyfills).
- For more information about Angular browser support, see [Browser Support](guide/browser-support).

View File

@ -495,7 +495,7 @@ for one turn of the browser's JavaScript cycle, which triggers a new change-dete
#### Write lean hook methods to avoid performance problems
When you run the *AfterView* sample, notice how frequently Angular calls `AfterViewChecked()`$emdash;often when there are no changes of interest.
When you run the *AfterView* sample, notice how frequently Angular calls `AfterViewChecked()`-often when there are no changes of interest.
Be very careful about how much logic or computation you put into one of these methods.
<div class="lightbox">

View File

@ -112,7 +112,7 @@ Because observables produce values asynchronously, try/catch will not effectivel
<code-example>
myObservable.subscribe({
next(num) { console.log('Next num: ' + num)},
error(err) { console.log('Received an errror: ' + err)}
error(err) { console.log('Received an error: ' + err)}
});
</code-example>

View File

@ -101,6 +101,7 @@ The following table provides the status for Angular versions under support.
Version | Status | Released | Active Ends | LTS Ends
------- | ------ | ------------ | ------------ | ------------
^10.0.0 | Active | Jun 24, 2020 | Dec 24, 2020 | Dec 24, 2021
^9.0.0 | Active | Feb 06, 2020 | Aug 06, 2020 | Aug 06, 2021
^8.0.0 | LTS | May 28, 2019 | Nov 28, 2019 | Nov 28, 2020

View File

@ -1,7 +1,7 @@
# Schematics for libraries
When you create an Angular library, you can provide and package it with schematics that integrate it with the Angular CLI.
With your schematics, your users can use `ng add` to install an initial version of your library,
With your schematics, your users can use `ng add` to install an initial version of your library,
`ng generate` to create artifacts defined in your library, and `ng update` to adjust their project for a new version of your library that introduces breaking changes.
All three types of schematics can be part of a collection that you package with your library.
@ -115,10 +115,10 @@ When you add a schematic to the collection, you have to point to it in the colle
<code-example header="projects/my-lib/schematics/my-service/schema.json (Schematic JSON Schema)" path="schematics-for-libraries/projects/my-lib/schematics/my-service/schema.json">
</code-example>
* *id* : A unique id for the schema in the collection.
* *title* : A human-readable description of the schema.
* *type* : A descriptor for the type provided by the properties.
* *properties* : An object that defines the available options for the schematic.
* *id*: A unique id for the schema in the collection.
* *title*: A human-readable description of the schema.
* *type*: A descriptor for the type provided by the properties.
* *properties*: An object that defines the available options for the schematic.
Each option associates key with a type, description, and optional alias.
The type defines the shape of the value you expect, and the description is displayed when the user requests usage help for your schematic.
@ -130,9 +130,9 @@ When you add a schematic to the collection, you have to point to it in the colle
<code-example header="projects/my-lib/schematics/my-service/schema.ts (Schematic Interface)" path="schematics-for-libraries/projects/my-lib/schematics/my-service/schema.ts">
</code-example>
* *name* : The name you want to provide for the created service.
* *path* : Overrides the path provided to the schematic. The default path value is based on the current working directory.
* *project* : Provides a specific project to run the schematic on. In the schematic, you can provide a default if the option is not provided by the user.
* *name*: The name you want to provide for the created service.
* *path*: Overrides the path provided to the schematic. The default path value is based on the current working directory.
* *project*: Provides a specific project to run the schematic on. In the schematic, you can provide a default if the option is not provided by the user.
### Add template files
@ -169,10 +169,9 @@ The Schematics framework provides a file templating system, which supports both
The system operates on placeholders defined inside files or paths that loaded in the input `Tree`.
It fills these in using values passed into the `Rule`.
For details of these data structure and syntax, see the [Schematics README](https://github.com/angular/angular-cli/blob/master/packages/angular_devkit/schematics/README.md).
For details of these data structures and syntax, see the [Schematics README](https://github.com/angular/angular-cli/blob/master/packages/angular_devkit/schematics/README.md).
1. Create the main file, `index.ts` and add the source code for your schematic factory function.
1. Create the main file `index.ts` and add the source code for your schematic factory function.
1. First, import the schematics definitions you will need. The Schematics framework offers many utility functions to create and use rules when running a schematic.
@ -271,7 +270,6 @@ For more information about rules and utility methods, see [Provided Rules](https
After you build your library and schematics, you can install the schematics collection to run against your project. The steps below show you how to generate a service using the schematic you created above.
### Build your library and schematics
From the root of your workspace, run the `ng build` command for your library.

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

View File

@ -596,6 +596,13 @@
"twitter": "devjoost",
"bio": "Joost is a Software Engineer from the Netherlands with an interest in open source software who likes to learn something new every day. He works at Blueriq during the day and contributes to Angular in his spare time, by working on the Angular compiler and runtime. He may review your PR even if you never asked for it ;)"
},
"sonukapoor": {
"name": "Sonu Kapoor",
"groups": ["Collaborators"],
"picture": "sonukapoor.jpg",
"website": "https://www.linkedin.com/in/sonu-kapoor/",
"bio": "Sonu is a Software Engineer from Toronto, with a high interest in front-end technologies and algorithms."
},
"jschwarty": {
"name": "Justin Schwartzenberger",
"picture": "justinschwartzenberger.jpg",
@ -815,5 +822,13 @@
"website": "https://wellwind.idv.tw/blog/",
"bio": "Mike is a full-stack developer, consultant, blogger, instructor, and conference speaker. He has over 10 years of web development experience and passion to share his knowledge.",
"groups": ["GDE"]
},
"ahasall": {
"name": "Amadou Sall",
"picture": "ahasall.jpg",
"groups": ["GDE"],
"twitter": "ahasall",
"website": "https://www.amadousall.com",
"bio": "Amadou is a Frontend Software Engineer from Senegal based in France. He currently works at Air France where he helps developers build better Angular applications. Passionate about web technologies, Amadou is an international speaker, a technical writer, and a Google Developer Expert in Angular."
}
}

View File

@ -506,80 +506,6 @@
"url": "guide/universal",
"title": "Server-side Rendering",
"tooltip": "Render HTML server-side with Angular Universal."
},
{
"title": "Upgrading from AngularJS",
"tooltip": "Incrementally upgrade an AngularJS application to Angular.",
"children": [
{
"url": "guide/upgrade-setup",
"title": "Setup for Upgrading from AngularJS",
"tooltip": "Use code from the Angular QuickStart seed as part of upgrading from AngularJS.",
"hidden": true
},
{
"url": "guide/upgrade",
"title": "Upgrading Instructions",
"tooltip": "Incrementally upgrade an AngularJS application to Angular."
},
{
"url": "guide/upgrade-performance",
"title": "Upgrading for Performance",
"tooltip": "Upgrade from AngularJS to Angular in a more flexible way."
},
{
"url": "guide/ajs-quick-reference",
"title": "AngularJS-Angular Concepts",
"tooltip": "Learn how AngularJS concepts and techniques map to Angular."
}
]
},
{
"title": "Angular Libraries",
"tooltip": "Extending Angular with shared libraries.",
"children": [
{
"url": "guide/libraries",
"title": "Libraries Overview",
"tooltip": "Understand how and when to use or create libraries."
},
{
"url": "guide/using-libraries",
"title": "Using Published Libraries",
"tooltip": "Integrate published libraries into an app."
},
{
"url": "guide/creating-libraries",
"title": "Creating Libraries",
"tooltip": "Extend Angular by creating, publishing, and using your own libraries."
}
]
},
{
"title": "Schematics",
"tooltip": "Using CLI schematics for code generation.",
"children": [
{
"url": "guide/schematics",
"title": "Schematics Overview",
"tooltip": "How the CLI uses schematics to generate code."
},
{
"url": "guide/schematics-authoring",
"title": "Authoring Schematics",
"tooltip": "Understand the structure of a schematic."
},
{
"url": "guide/schematics-for-libraries",
"title": "Schematics for Libraries",
"tooltip": "Use schematics to integrate your library with the Angular CLI."
}
]
},
{
"url": "guide/cli-builder",
"title": "CLI Builders",
"tooltip": "Using builders to customize Angular CLI."
}
]
},
@ -1008,6 +934,10 @@
}
],
"docVersions": [
{
"title": "v9",
"url": "https://v9.angular.io/"
},
{
"title": "v8",
"url": "https://v8.angular.io/"

View File

@ -6,7 +6,7 @@
In this tutorial, you build your own app from the ground up, providing experience with the typical development process, as well as an introduction to basic app-design concepts, tools, and terminology.
If you're completely new to Angular, you might want to try the [**Try it now**](start) quick-start app first.
It is based on a ready-made partially-completed project, which you can examine and modify in the StacBlitz interactive development environment, where you can see the results in real time.
It is based on a ready-made partially-completed project, which you can examine and modify in the StackBlitz interactive development environment, where you can see the results in real time.
The "Try it" tutorial covers the same major topics&mdash;components, template syntax, routing, services, and accessing data via HTTP&mdash;in a condensed format, following the most current best practices.

View File

@ -1,5 +1,6 @@
{
"hosting": {
"target": "aio",
"public": "dist",
"cleanUrls": true,
"redirects": [
@ -127,7 +128,7 @@
// The below paths are referenced in users projects generated by the CLI
{"type": 301, "source": "/config/tsconfig", "destination": "/guide/typescript-configuration"},
{"type": 301, "source": "/config/solution-tsconfig", "destination": "https://devblogs.microsoft.com/typescript/announcing-typescript-3-9/#solution-style-tsconfig"},
{"type": 301, "source": "/config/app-package-json", "destination": "https://webpack.js.org/configuration/optimization/#optimizationsideeffects"}
{"type": 301, "source": "/config/app-package-json", "destination": "/guide/strict-mode#non-local-side-effects-in-applications"}
],
"rewrites": [
{

View File

@ -123,7 +123,7 @@
"cross-spawn": "^5.1.0",
"css-selector-parser": "^1.3.0",
"dgeni": "^0.4.11",
"dgeni-packages": "^0.28.3",
"dgeni-packages": "^0.28.4",
"entities": "^1.1.1",
"eslint": "^3.19.0",
"eslint-plugin-jasmine": "^2.2.0",
@ -175,4 +175,4 @@
"xregexp": "^4.0.0",
"yargs": "^7.0.2"
}
}
}

View File

@ -33,7 +33,7 @@ else
readonly majorVersionStable=${CI_STABLE_BRANCH%%.*}
# Do not deploy if the major version is not less than the stable branch major version
if [[ !( "$majorVersion" -lt "$majorVersionStable" ) ]]; then
if (( $majorVersion >= $majorVersionStable )); then
echo "Skipping deploy of branch \"$CI_BRANCH\" to firebase."
echo "We only deploy archive branches with the major version less than the stable branch: \"$CI_STABLE_BRANCH\""
exit 0
@ -64,16 +64,27 @@ fi
case $deployEnv in
next)
readonly projectId=aio-staging
readonly siteId=$projectId
readonly deployedUrl=https://next.angular.io/
readonly firebaseToken=$CI_SECRET_AIO_DEPLOY_FIREBASE_TOKEN
;;
stable)
readonly projectId=angular-io
readonly siteId=$projectId
readonly deployedUrl=https://angular.io/
readonly firebaseToken=$CI_SECRET_AIO_DEPLOY_FIREBASE_TOKEN
;;
archive)
readonly projectId=v${majorVersion}-angular-io
# Special case v9-angular-io because its piloting the firebase hosting "multisites" setup
# See https://angular-team.atlassian.net/browse/DEV-125 for more info.
if [[ "$majorVersion" == "9" ]]; then
readonly projectId=aio-staging
readonly siteId=v9-angular-io
else
readonly projectId=v${majorVersion}-angular-io
readonly siteId=$projectId
fi
readonly deployedUrl=https://v${majorVersion}.angular.io/
readonly firebaseToken=$CI_SECRET_AIO_DEPLOY_FIREBASE_TOKEN
;;
@ -82,6 +93,7 @@ esac
echo "Git branch : $CI_BRANCH"
echo "Build/deploy mode : $deployEnv"
echo "Firebase project : $projectId"
echo "Firebase site : $siteId"
echo "Deployment URL : $deployedUrl"
if [[ ${1:-} == "--dry-run" ]]; then
@ -92,23 +104,29 @@ fi
(
cd "`dirname $0`/.."
# Build the app
echo "\n\n\n==== Build the aio app ====\n"
yarn build --configuration=$deployEnv --progress=false
# Include any mode-specific files
echo "\n\n\n==== Add any mode-specific files into the aio distribution ====\n"
cp -rf src/extra-files/$deployEnv/. dist/
# Set deployedUrl as parameter in the opensearch description
echo "\n\n\n==== Update opensearch descriptor for aio with the deployedUrl ====\n"
# deployedUrl must end with /
yarn set-opensearch-url $deployedUrl
# Check payload size
echo "\n\n\n==== Check payload size and upload the numbers to firebase db ====\n"
yarn payload-size
# Deploy to Firebase
yarn firebase use "$projectId" --token "$firebaseToken"
yarn firebase deploy --message "Commit: $CI_COMMIT" --non-interactive --token "$firebaseToken"
# Run PWA-score tests
echo "\n\n\n==== Deploy aio to firebase hosting ====\n"
yarn firebase use "${projectId}" --token "$firebaseToken"
yarn firebase target:apply hosting aio $siteId --token "$firebaseToken"
yarn firebase deploy --only hosting:aio --message "Commit: $CI_COMMIT" --non-interactive --token "$firebaseToken"
echo "\n\n\n==== Run PWA-score tests ====\n"
yarn test-pwa-score "$deployedUrl" "$CI_AIO_MIN_PWA_SCORE"
)

View File

@ -68,6 +68,7 @@ function check {
expected="Git branch : master
Build/deploy mode : next
Firebase project : aio-staging
Firebase site : aio-staging
Deployment URL : https://next.angular.io/"
check "$actual" "$expected"
)
@ -103,6 +104,7 @@ Deployment URL : https://next.angular.io/"
expected="Git branch : 4.3.x
Build/deploy mode : stable
Firebase project : angular-io
Firebase site : angular-io
Deployment URL : https://angular.io/"
check "$actual" "$expected"
)
@ -139,10 +141,37 @@ Deployment URL : https://angular.io/"
expected="Git branch : 2.4.x
Build/deploy mode : archive
Firebase project : v2-angular-io
Firebase site : v2-angular-io
Deployment URL : https://v2.angular.io/"
check "$actual" "$expected"
)
(
echo ===== archive - v9-angular-io multisite special case - deploy success
actual=$(
export BASH_ENV=/dev/null
export CI_REPO_OWNER=angular
export CI_REPO_NAME=angular
export CI_PULL_REQUEST=false
export CI_BRANCH=9.1.x
export CI_STABLE_BRANCH=10.0.x
export CI_COMMIT=$(git ls-remote origin 9.1.x | cut -c1-40)
export CI_SECRET_AIO_DEPLOY_FIREBASE_TOKEN=XXXXX
$deployToFirebaseDryRun
)
expected="Git branch : 9.1.x
Build/deploy mode : archive
Firebase project : aio-staging
Firebase site : v9-angular-io
Deployment URL : https://v9.angular.io/"
# TODO: This test incorrectly expects the Firebase project to be v9-angular-io.
# v9-angular-io is a "multisites" project currently within the aio-staging project
# This setup is temporary and was created in order to deploy v9.angular.io without
# disruptions.
# See https://angular-team.atlassian.net/browse/DEV-125 for more info.
check "$actual" "$expected"
)
(
echo ===== archive - skip deploy - commit not HEAD
actual=$(

View File

@ -0,0 +1,52 @@
import { by, element } from 'protractor';
import { SitePage } from './app.po';
describe('api-list', () => {
const apiSearchInput = element(by.css('aio-api-list .form-search input'));
const apiStatusDropdown = element(by.css('aio-api-list aio-select[label="Status:"]'));
const apiTypeDropdown = element(by.css('aio-api-list aio-select[label="Type:"]'));
let page: SitePage;
beforeEach(() => {
page = new SitePage();
page.navigateTo('api');
});
it('should find AnimationSequenceMetadata when searching by partial word anima', () => {
expect(page.getApiSearchResults()).toContain('HttpEventType');
apiSearchInput.clear();
apiSearchInput.sendKeys('anima');
expect(page.getApiSearchResults()).not.toContain('HttpEventType');
expect(page.getApiSearchResults()).toContain('AnimationSequenceMetadata');
});
it('should find getLocaleDateTimeFormat when searching by partial word date', () => {
expect(page.getApiSearchResults()).toContain('formatCurrency');
apiSearchInput.clear();
apiSearchInput.sendKeys('date');
expect(page.getApiSearchResults()).not.toContain('formatCurrency');
expect(page.getApiSearchResults()).toContain('getLocaleDateTimeFormat');
});
it('should find LowerCasePipe when searching for type pipe', () => {
expect(page.getApiSearchResults()).toContain('getLocaleDateTimeFormat');
page.clickDropdownItem(apiTypeDropdown, 'Pipe');
expect(page.getApiSearchResults()).not.toContain('getLocaleDateTimeFormat');
expect(page.getApiSearchResults()).toContain('LowerCasePipe');
});
it('should find ElementRef when searching for status Security Risk', () => {
expect(page.getApiSearchResults()).toContain('getLocaleDateTimeFormat');
page.clickDropdownItem(apiStatusDropdown, 'Security Risk');
expect(page.getApiSearchResults()).not.toContain('getLocaleDateTimeFormat');
expect(page.getApiSearchResults()).toContain('ElementRef');
});
});

View File

@ -83,4 +83,16 @@ export class SitePage {
browser.wait(ExpectedConditions.presenceOf(results.first()), 8000);
return results.map(link => link && link.getText());
}
getApiSearchResults() {
const results = element.all(by.css('aio-api-list .api-item'));
browser.wait(ExpectedConditions.presenceOf(results.first()), 2000);
return results.map(elem => elem && elem.getText());
}
clickDropdownItem(dropdown: ElementFinder, itemName: string){
dropdown.element(by.css('.form-select-button')).click();
const menuItem = dropdown.element(by.cssContainingText('.form-select-dropdown li', itemName));
menuItem.click();
}
}

View File

@ -4467,10 +4467,10 @@ dezalgo@^1.0.0:
asap "^2.0.0"
wrappy "1"
dgeni-packages@^0.28.3:
version "0.28.3"
resolved "https://registry.yarnpkg.com/dgeni-packages/-/dgeni-packages-0.28.3.tgz#2e1e55f341c389b67ebb28933ce1e7e9ad05c49b"
integrity sha512-WyVzY3Q4ylfnc2677le5G7a7WqkF88rBSjU9IrAofqro71yzZeWLoEdr/gJY+lJZ0PrDyuRW05pFvIbvX8N0PQ==
dgeni-packages@^0.28.4:
version "0.28.4"
resolved "https://registry.yarnpkg.com/dgeni-packages/-/dgeni-packages-0.28.4.tgz#53a3e6700b8d8f6be168cadcc9fdb36e1d7011d3"
integrity sha512-7AUG3pKpWtn69c3v2Mzgh+i5gd+L0AxFfYGWGzBdlJqMlQfaQPQjaS54iYCvnOlK9rXBn9j39yO6EU70gDZuFw==
dependencies:
canonical-path "^1.0.0"
catharsis "^0.8.1"

View File

@ -25,6 +25,7 @@ def component_benchmark(
driver_deps,
ng_srcs,
ng_deps,
ng_assets = [],
assets = None,
styles = None,
entry_point = None,
@ -65,6 +66,7 @@ def component_benchmark(
driver_deps: Driver's dependencies
ng_srcs: All of the ts srcs for the angular app
ng_deps: Dependencies for the angular app
ng_assets: The static assets for the angular app
assets: Static files
styles: Stylesheets
entry_point: Main entry point for the angular app
@ -104,6 +106,7 @@ def component_benchmark(
ng_module(
name = app_lib,
srcs = ng_srcs,
assets = ng_assets,
# Creates ngFactory and ngSummary to be imported by the app's entry point.
generate_ve_shims = True,
deps = ng_deps,

View File

@ -11,6 +11,7 @@ import {assertNoErrors, getConfig, NgDevConfig} from '../utils/config';
export interface CommitMessageConfig {
maxLineLength: number;
minBodyLength: number;
minBodyLengthTypeExcludes?: string[];
types: string[];
scopes: string[];
}
@ -19,7 +20,7 @@ export interface CommitMessageConfig {
export function getCommitMessageConfig() {
// List of errors encountered validating the config.
const errors: string[] = [];
// The unvalidated config object.
// The non-validated config object.
const config: Partial<NgDevConfig<{commitMessage: CommitMessageConfig}>> = getConfig();
if (config.commitMessage === undefined) {

View File

@ -10,19 +10,22 @@
import * as validateConfig from './config';
import {validateCommitMessage} from './validate';
type CommitMessageConfig = validateConfig.CommitMessageConfig;
// Constants
const config = {
'commitMessage': {
'maxLineLength': 120,
'minBodyLength': 0,
'types': [
const config: {commitMessage: CommitMessageConfig} = {
commitMessage: {
maxLineLength: 120,
minBodyLength: 0,
types: [
'feat',
'fix',
'refactor',
'release',
'style',
],
'scopes': [
scopes: [
'common',
'compiler',
'core',
@ -224,5 +227,42 @@ describe('validate-commit-message.js', () => {
});
});
});
describe('minBodyLength', () => {
const minBodyLengthConfig: {commitMessage: CommitMessageConfig} = {
commitMessage: {
maxLineLength: 120,
minBodyLength: 30,
minBodyLengthTypeExcludes: ['docs'],
types: ['fix', 'docs'],
scopes: ['core']
}
};
beforeEach(() => {
(validateConfig.getCommitMessageConfig as jasmine.Spy).and.returnValue(minBodyLengthConfig);
});
it('should fail validation if the body is shorter than `minBodyLength`', () => {
expect(validateCommitMessage(
'fix(core): something\n\n Explanation of the motivation behind this change'))
.toBe(VALID);
expect(validateCommitMessage('fix(core): something\n\n too short')).toBe(INVALID);
expect(lastError).toContain(
'The commit message body does not meet the minimum length of 30 characters');
expect(validateCommitMessage('fix(core): something')).toBe(INVALID);
expect(lastError).toContain(
'The commit message body does not meet the minimum length of 30 characters');
});
it('should pass validation if the body is shorter than `minBodyLength` but the commit type is in the `minBodyLengthTypeExclusions` list',
() => {
expect(validateCommitMessage('docs: just fixing a typo')).toBe(VALID);
expect(validateCommitMessage('docs(core): just fixing a typo')).toBe(VALID);
expect(validateCommitMessage(
'docs(core): just fixing a typo\n\nThis was just a silly typo.'))
.toBe(VALID);
});
});
});
});

View File

@ -148,7 +148,8 @@ export function validateCommitMessage(
// Checking commit body //
//////////////////////////
if (commit.bodyWithoutLinking.trim().length < config.minBodyLength) {
if (!config.minBodyLengthTypeExcludes?.includes(commit.type) &&
commit.bodyWithoutLinking.trim().length < config.minBodyLength) {
printError(`The commit message body does not meet the minimum length of ${
config.minBodyLength} characters`);
return false;
@ -157,7 +158,7 @@ export function validateCommitMessage(
const bodyByLine = commit.body.split('\n');
if (bodyByLine.some(line => line.length > config.maxLineLength)) {
printError(
`The commit messsage body contains lines greater than ${config.maxLineLength} characters`);
`The commit message body contains lines greater than ${config.maxLineLength} characters`);
return false;
}

View File

@ -63,8 +63,8 @@ export async function discoverNewConflictsForPr(
process.exit(1);
}
/** The active github branch when the run began. */
const originalBranch = git.getCurrentBranch();
/** The active github branch or revision before we performed any Git commands. */
const previousBranchOrRevision = git.getCurrentBranchOrRevision();
/* Progress bar to indicate progress. */
const progressBar = new Bar({format: `[{bar}] ETA: {eta}s | {value}/{total}`});
/* PRs which were found to be conflicting. */
@ -103,7 +103,7 @@ export async function discoverNewConflictsForPr(
const result = exec(`git rebase FETCH_HEAD`);
if (result.code) {
error('The requested PR currently has conflicts');
cleanUpGitState(originalBranch);
cleanUpGitState(previousBranchOrRevision);
process.exit(1);
}
@ -130,7 +130,7 @@ export async function discoverNewConflictsForPr(
info();
info(`Result:`);
cleanUpGitState(originalBranch);
cleanUpGitState(previousBranchOrRevision);
// If no conflicts are found, exit successfully.
if (conflicts.length === 0) {
@ -147,14 +147,14 @@ export async function discoverNewConflictsForPr(
process.exit(1);
}
/** Reset git back to the provided branch. */
export function cleanUpGitState(branch: string) {
/** Reset git back to the provided branch or revision. */
export function cleanUpGitState(previousBranchOrRevision: string) {
// Ensure that any outstanding rebases are aborted.
exec(`git rebase --abort`);
// Ensure that any changes in the current repo state are cleared.
exec(`git reset --hard`);
// Checkout the original branch from before the run began.
exec(`git checkout ${branch}`);
exec(`git checkout ${previousBranchOrRevision}`);
// Delete the generated branch.
exec(`git branch -D ${tempWorkingBranch}`);
}

View File

@ -59,7 +59,7 @@ export class AutosquashMergeStrategy extends MergeStrategy {
// is desired, we set the `GIT_SEQUENCE_EDITOR` environment variable to `true` so that
// the rebase seems interactive to Git, while it's not interactive to the user.
// See: https://github.com/git/git/commit/891d4a0313edc03f7e2ecb96edec5d30dc182294.
const branchBeforeRebase = this.git.getCurrentBranch();
const branchOrRevisionBeforeRebase = this.git.getCurrentBranchOrRevision();
const rebaseEnv =
needsCommitMessageFixup ? undefined : {...process.env, GIT_SEQUENCE_EDITOR: 'true'};
this.git.run(
@ -69,9 +69,9 @@ export class AutosquashMergeStrategy extends MergeStrategy {
// Update pull requests commits to reference the pull request. This matches what
// Github does when pull requests are merged through the Web UI. The motivation is
// that it should be easy to determine which pull request contained a given commit.
// **Note**: The filter-branch command relies on the working tree, so we want to make
// sure that we are on the initial branch where the merge script has been run.
this.git.run(['checkout', '-f', branchBeforeRebase]);
// Note: The filter-branch command relies on the working tree, so we want to make sure
// that we are on the initial branch or revision where the merge script has been invoked.
this.git.run(['checkout', '-f', branchOrRevisionBeforeRebase]);
this.git.run(
['filter-branch', '-f', '--msg-filter', `${MSG_FILTER_SCRIPT} ${prNumber}`, revisionRange]);

View File

@ -76,14 +76,14 @@ export class PullRequestMergeTask {
new GithubApiMergeStrategy(this.git, this.config.githubApiMerge) :
new AutosquashMergeStrategy(this.git);
// Branch that is currently checked out so that we can switch back to it once
// the pull request has been merged.
let previousBranch: null|string = null;
// Branch or revision that is currently checked out so that we can switch back to
// it once the pull request has been merged.
let previousBranchOrRevision: null|string = null;
// The following block runs Git commands as child processes. These Git commands can fail.
// We want to capture these command errors and return an appropriate merge request status.
try {
previousBranch = this.git.getCurrentBranch();
previousBranchOrRevision = this.git.getCurrentBranchOrRevision();
// Run preparations for the merge (e.g. fetching branches).
await strategy.prepare(pullRequest);
@ -96,7 +96,7 @@ export class PullRequestMergeTask {
// Switch back to the previous branch. We need to do this before deleting the temporary
// branches because we cannot delete branches which are currently checked out.
this.git.run(['checkout', '-f', previousBranch]);
this.git.run(['checkout', '-f', previousBranchOrRevision]);
await strategy.cleanup(pullRequest);
@ -112,8 +112,8 @@ export class PullRequestMergeTask {
} finally {
// Always try to restore the branch if possible. We don't want to leave
// the repository in a different state than before.
if (previousBranch !== null) {
this.git.runGraceful(['checkout', '-f', previousBranch]);
if (previousBranchOrRevision !== null) {
this.git.runGraceful(['checkout', '-f', previousBranchOrRevision]);
}
}
}

View File

@ -50,10 +50,10 @@ export async function rebasePr(
}
/**
* The branch originally checked out before this method performs any Git
* operations that may change the working branch.
* The branch or revision originally checked out before this method performed
* any Git operations that may change the working branch.
*/
const originalBranch = git.getCurrentBranch();
const previousBranchOrRevision = git.getCurrentBranchOrRevision();
/* Get the PR information from Github. */
const pr = await getPr(PR_SCHEMA, prNumber, config.github);
@ -121,7 +121,7 @@ export async function rebasePr(
info();
info(`To abort the rebase and return to the state of the repository before this command`);
info(`run the following command:`);
info(` $ git rebase --abort && git reset --hard && git checkout ${originalBranch}`);
info(` $ git rebase --abort && git reset --hard && git checkout ${previousBranchOrRevision}`);
process.exit(1);
} else {
info(`Cleaning up git state, and restoring previous state.`);
@ -137,7 +137,7 @@ export async function rebasePr(
// Ensure that any changes in the current repo state are cleared.
git.runGraceful(['reset', '--hard'], {stdio: 'ignore'});
// Checkout the original branch from before the run began.
git.runGraceful(['checkout', originalBranch], {stdio: 'ignore'});
git.runGraceful(['checkout', previousBranchOrRevision], {stdio: 'ignore'});
}
}

View File

@ -130,9 +130,16 @@ export class GitClient {
return this.run(['branch', branchName, '--contains', sha]).stdout !== '';
}
/** Gets the currently checked out branch. */
getCurrentBranch(): string {
return this.run(['rev-parse', '--abbrev-ref', 'HEAD']).stdout.trim();
/** Gets the currently checked out branch or revision. */
getCurrentBranchOrRevision(): string {
const branchName = this.run(['rev-parse', '--abbrev-ref', 'HEAD']).stdout.trim();
// If no branch name could be resolved. i.e. `HEAD` has been returned, then Git
// is currently in a detached state. In those cases, we just want to return the
// currently checked out revision/SHA.
if (branchName === 'HEAD') {
return this.run(['rev-parse', 'HEAD']).stdout.trim();
}
return branchName;
}
/** Gets whether the current Git repository has uncommitted changes. */

View File

@ -53,40 +53,45 @@ If you modify any part of a public API in one of the supported public packages,
The public API guard provides a Bazel target that updates the current status of a given package. If you add to or modify the public API in any way, you must use [yarn](https://yarnpkg.com/) to execute the Bazel target in your terminal shell of choice (a recent version of `bash` is recommended).
```shell
yarn bazel run //tools/public_api_guard:<modified_package>_api.accept
yarn bazel run //packages/<modified_package>:<modified_package>_api.accept
```
Using yarn ensures that you are running the correct version of Bazel.
(Read more about building Angular with Bazel [here](./BAZEL.md).)
Here is an example of a Circle CI test failure that resulted from adding a new allowed type to a public property in `forms.d.ts`. Error messages from the API guard use [`git-diff` formatting](https://git-scm.com/docs/git-diff#_combined_diff_format).
Here is an example of a Circle CI test failure that resulted from adding a new allowed type to a public property in `core.d.ts`. Error messages from the API guard use [`git-diff` formatting](https://git-scm.com/docs/git-diff#_combined_diff_format).
```
FAIL: //tools/public_api_guard:forms_api (see /home/circleci/.cache/bazel/_bazel_circleci/9ce5c2144ecf75d11717c0aa41e45a8d/execroot/angular/bazel-out/k8-fastbuild/testlogs/tools/public_api_guard/forms_api/test_attempts/attempt_1.log)
FAIL: //tools/public_api_guard:forms_api (see /home/circleci/.cache/bazel/_bazel_circleci/9ce5c2144ecf75d11717c0aa41e45a8d/execroot/angular/bazel-out/k8-fastbuild/testlogs/tools/public_api_guard/forms_api/test.log)
FAIL: //packages/core:core_api (see /home/circleci/.cache/bazel/_bazel_circleci/9ce5c2144ecf75d11717c0aa41e45a8d/execroot/angular/bazel-out/k8-fastbuild/testlogs/packages/core/core_api/test_attempts/attempt_1.log)
INFO: From Action packages/compiler-cli/ngcc/test/fesm5_angular_core.js:
[BABEL] Note: The code generator has deoptimised the styling of /b/f/w/bazel-out/k8-fastbuild/bin/packages/core/npm_package/fesm2015/core.js as it exceeds the max of 500KB.
FAIL: //packages/core:core_api (see /home/circleci/.cache/bazel/_bazel_circleci/9ce5c2144ecf75d11717c0aa41e45a8d/execroot/angular/bazel-out/k8-fastbuild/testlogs/packages/core/core_api/test.log)
FAILED: //packages/core:core_api (Summary)
/home/circleci/.cache/bazel/_bazel_circleci/9ce5c2144ecf75d11717c0aa41e45a8d/execroot/angular/bazel-out/k8-fastbuild/testlogs/packages/core/core_api/test.log
/home/circleci/.cache/bazel/_bazel_circleci/9ce5c2144ecf75d11717c0aa41e45a8d/execroot/angular/bazel-out/k8-fastbuild/testlogs/packages/core/core_api/test_attempts/attempt_1.log
INFO: From Testing //packages/core:core_api:
==================== Test output for //packages/core:core_api:
/b/f/w/bazel-out/k8-fastbuild/bin/packages/core/core_api.sh.runfiles/angular/packages/core/npm_package/core.d.ts(7,1): error: No export declaration found for symbol "ComponentFactory"
--- goldens/public-api/core/core.d.ts Golden file
+++ goldens/public-api/core/core.d.ts Generated API
@@ -563,9 +563,9 @@
ngModule: Type<T>;
providers?: Provider[];
}
-export declare type NgIterable<T> = Array<T> | Iterable<T>;
+export declare type NgIterable<T> = Iterable<T>;
export declare interface NgModule {
bootstrap?: Array<Type<any> | any[]>;
declarations?: Array<Type<any> | any[]>;
FAILED: //tools/public_api_guard:forms_api (Summary)
/home/circleci/.cache/bazel/_bazel_circleci/9ce5c2144ecf75d11717c0aa41e45a8d/execroot/angular/bazel-out/k8-fastbuild/testlogs/tools/public_api_guard/forms_api/test.log
/home/circleci/.cache/bazel/_bazel_circleci/9ce5c2144ecf75d11717c0aa41e45a8d/execroot/angular/bazel-out/k8-fastbuild/testlogs/tools/public_api_guard/forms_api/test_attempts/attempt_1.log
INFO: From Testing //tools/public_api_guard:forms_api:
==================== Test output for //tools/public_api_guard:forms_api:
--- tools/public_api_guard/forms/forms.d.ts Golden file
+++ tools/public_api_guard/forms/forms.d.ts Generated API
@@ -4,9 +4,9 @@
readonly disabled: boolean;
readonly enabled: boolean;
readonly errors: ValidationErrors | null;
readonly invalid: boolean;
- readonly parent: FormGroup | FormArray;
+ readonly parent: FormGroup | FormArray | undefined;
readonly pending: boolean;
readonly pristine: boolean;
readonly root: AbstractControl;
readonly status: string;
If you modify a public API, you must accept the new golden file.
To do so, execute the following Bazel target:
yarn bazel run //tools/public_api_guard:forms_api.accept
yarn bazel run //packages/core:core_api.accept
```

View File

@ -398,7 +398,7 @@ export declare class RouterLinkActive implements OnChanges, OnDestroy, AfterCont
routerLinkActiveOptions: {
exact: boolean;
};
constructor(router: Router, element: ElementRef, renderer: Renderer2, link?: RouterLink | undefined, linkWithHref?: RouterLinkWithHref | undefined);
constructor(router: Router, element: ElementRef, renderer: Renderer2, cdr: ChangeDetectorRef, link?: RouterLink | undefined, linkWithHref?: RouterLinkWithHref | undefined);
ngAfterContentInit(): void;
ngOnChanges(changes: SimpleChanges): void;
ngOnDestroy(): void;

View File

@ -12,7 +12,7 @@
"master": {
"uncompressed": {
"runtime-es2015": 2987,
"main-es2015": 451406,
"main-es2015": 450883,
"polyfills-es2015": 52630
}
}
@ -21,8 +21,8 @@
"master": {
"uncompressed": {
"runtime-es2015": 3097,
"main-es2015": 428886,
"polyfills-es2015": 52195
"main-es2015": 428031,
"polyfills-es2015": 52261
}
}
}

View File

@ -30,8 +30,8 @@
"master": {
"uncompressed": {
"runtime-es2015": 1485,
"main-es2015": 136302,
"polyfills-es2015": 37246
"main-es2015": 135533,
"polyfills-es2015": 37248
}
}
},
@ -39,7 +39,7 @@
"master": {
"uncompressed": {
"runtime-es2015": 2289,
"main-es2015": 246085,
"main-es2015": 245488,
"polyfills-es2015": 36938,
"5-es2015": 751
}
@ -62,7 +62,7 @@
"bundle": "TODO(i): we should define ngDevMode to false in Closure, but --define only works in the global scope.",
"bundle": "TODO(i): (FW-2164) TS 3.9 new class shape seems to have broken Closure in big ways. The size went from 169991 to 252338",
"bundle": "TODO(i): after removal of tsickle from ngc-wrapped / ng_package, we had to switch to SIMPLE optimizations which increased the size from 252338 to 1198917, see PR#37221 and PR#37317 for more info",
"bundle": 1209688
"bundle": 1209651
}
}
}

View File

@ -4,8 +4,10 @@ import {
ElementRef,
HostBinding,
HostListener,
Injectable,
Input,
NgModule
NgModule,
Pipe
} from '@angular/core';
export class NonAngularBaseClass {
@ -76,3 +78,17 @@ export class UndecoratedPipeBase {
export class WithDirectiveLifecycleHook {
ngOnInit() {}
}
// This class is already decorated and should not be migrated. i.e. no TODO
// or Angular decorator should be added. `@Injectable` is sufficient.
@Injectable()
export class MyService {
ngOnDestroy() {}
}
// This class is already decorated and should not be migrated. i.e. no TODO
// or Angular decorator should be added. `@Injectable` is sufficient.
@Pipe({name: 'my-pipe'})
export class MyPipe {
ngOnDestroy() {}
}

View File

@ -4,8 +4,10 @@ import {
ElementRef,
HostBinding,
HostListener,
Injectable,
Input,
NgModule
NgModule,
Pipe
} from '@angular/core';
export class NonAngularBaseClass {
@ -87,3 +89,17 @@ export class UndecoratedPipeBase {
export class WithDirectiveLifecycleHook {
ngOnInit() {}
}
// This class is already decorated and should not be migrated. i.e. no TODO
// or Angular decorator should be added. `@Injectable` is sufficient.
@Injectable()
export class MyService {
ngOnDestroy() {}
}
// This class is already decorated and should not be migrated. i.e. no TODO
// or Angular decorator should be added. `@Injectable` is sufficient.
@Pipe({name: 'my-pipe'})
export class MyPipe {
ngOnDestroy() {}
}

View File

@ -42,7 +42,7 @@ module.exports = function(config) {
// Including systemjs because it defines `__eval`, which produces correct stack traces.
'test-events.js',
'shims_for_IE.js',
'third_party/shims_for_IE.js',
'node_modules/systemjs/dist/system.src.js',
// Serve polyfills necessary for testing the `elements` package.

View File

@ -1,6 +1,6 @@
{
"name": "angular-srcs",
"version": "10.0.0",
"version": "10.0.2",
"private": true,
"description": "Angular - a web framework for modern web apps",
"homepage": "https://github.com/angular/angular",

View File

@ -27,5 +27,8 @@
"bugs": {
"url": "https://github.com/angular/angular/issues"
},
"homepage": "https://github.com/angular/angular/tree/master/packages/compiler-cli"
"homepage": "https://github.com/angular/angular/tree/master/packages/compiler-cli",
"publishConfig": {
"registry": "https://wombat-dressing-room.appspot.com"
}
}

View File

@ -339,7 +339,9 @@ export class HttpXhrBackend implements HttpBackend {
}
// Finally, abort the in-flight request.
xhr.abort();
if (xhr.readyState !== xhr.DONE) {
xhr.abort();
}
};
});
}

View File

@ -147,6 +147,17 @@ const XSSI_PREFIX = ')]}\'\n';
});
factory.mock.mockErrorEvent(new Error('blah'));
});
it('avoids abort a request when fetch operation is completed', done => {
const abort = jasmine.createSpy('abort');
backend.handle(TEST_POST).toPromise().then(() => {
expect(abort).not.toHaveBeenCalled();
done();
});
factory.mock.abort = abort;
factory.mock.mockFlush(200, 'OK', 'Done');
});
describe('progress events', () => {
it('are emitted for download progress', done => {
backend.handle(TEST_POST.clone({reportProgress: true}))

View File

@ -31,6 +31,7 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/indexer",
"//packages/compiler-cli/src/ngtsc/perf",
"//packages/compiler-cli/src/ngtsc/reflection",
"//packages/compiler-cli/src/ngtsc/shims",
"//packages/compiler-cli/src/ngtsc/typecheck",
"@npm//@bazel/typescript",
"@npm//@types/node",

View File

@ -12,7 +12,7 @@ import {ParsedConfiguration} from '../../..';
import {ComponentDecoratorHandler, DirectiveDecoratorHandler, InjectableDecoratorHandler, NgModuleDecoratorHandler, PipeDecoratorHandler, ReferencesRegistry, ResourceLoader} from '../../../src/ngtsc/annotations';
import {CycleAnalyzer, ImportGraph} from '../../../src/ngtsc/cycles';
import {isFatalDiagnosticError} from '../../../src/ngtsc/diagnostics';
import {absoluteFrom, dirname, FileSystem, LogicalFileSystem, resolve} from '../../../src/ngtsc/file_system';
import {absoluteFrom, absoluteFromSourceFile, dirname, FileSystem, LogicalFileSystem, resolve} from '../../../src/ngtsc/file_system';
import {AbsoluteModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ModuleResolver, NOOP_DEFAULT_IMPORT_RECORDER, PrivateExportAliasingHost, Reexport, ReferenceEmitter} from '../../../src/ngtsc/imports';
import {CompoundMetadataReader, CompoundMetadataRegistry, DtsMetadataReader, InjectableClassRegistry, LocalMetadataRegistry} from '../../../src/ngtsc/metadata';
import {PartialEvaluator} from '../../../src/ngtsc/partial_evaluator';
@ -148,7 +148,8 @@ export class DecorationAnalyzer {
*/
analyzeProgram(): DecorationAnalyses {
for (const sourceFile of this.program.getSourceFiles()) {
if (!sourceFile.isDeclarationFile && isWithinPackage(this.packagePath, sourceFile)) {
if (!sourceFile.isDeclarationFile &&
isWithinPackage(this.packagePath, absoluteFromSourceFile(sourceFile))) {
this.compiler.analyzeFile(sourceFile);
}
}

View File

@ -7,7 +7,7 @@
*/
import * as ts from 'typescript';
import {AbsoluteFsPath} from '../../../src/ngtsc/file_system';
import {absoluteFromSourceFile, AbsoluteFsPath} from '../../../src/ngtsc/file_system';
import {MetadataReader} from '../../../src/ngtsc/metadata';
import {PartialEvaluator} from '../../../src/ngtsc/partial_evaluator';
import {ClassDeclaration, Decorator} from '../../../src/ngtsc/reflection';
@ -44,7 +44,7 @@ export class DefaultMigrationHost implements MigrationHost {
}
isInScope(clazz: ClassDeclaration): boolean {
return isWithinPackage(this.entryPointPath, clazz.getSourceFile());
return isWithinPackage(this.entryPointPath, absoluteFromSourceFile(clazz.getSourceFile()));
}
}

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import * as ts from 'typescript';
import {AbsoluteFsPath} from '../../../src/ngtsc/file_system';
import {absoluteFromSourceFile, AbsoluteFsPath} from '../../../src/ngtsc/file_system';
import {NgccReflectionHost, SwitchableVariableDeclaration} from '../host/ngcc_host';
import {isWithinPackage} from './util';
@ -35,7 +35,7 @@ export class SwitchMarkerAnalyzer {
analyzeProgram(program: ts.Program): SwitchMarkerAnalyses {
const analyzedFiles = new SwitchMarkerAnalyses();
program.getSourceFiles()
.filter(sourceFile => isWithinPackage(this.packagePath, sourceFile))
.filter(sourceFile => isWithinPackage(this.packagePath, absoluteFromSourceFile(sourceFile)))
.forEach(sourceFile => {
const declarations = this.host.getSwitchableDeclarations(sourceFile);
if (declarations.length) {

View File

@ -5,13 +5,11 @@
* 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 * as ts from 'typescript';
import {absoluteFromSourceFile, AbsoluteFsPath, relative} from '../../../src/ngtsc/file_system';
import {AbsoluteFsPath, relative} from '../../../src/ngtsc/file_system';
import {DependencyTracker} from '../../../src/ngtsc/incremental/api';
export function isWithinPackage(packagePath: AbsoluteFsPath, sourceFile: ts.SourceFile): boolean {
const relativePath = relative(packagePath, absoluteFromSourceFile(sourceFile));
export function isWithinPackage(packagePath: AbsoluteFsPath, filePath: AbsoluteFsPath): boolean {
const relativePath = relative(packagePath, filePath);
return !relativePath.startsWith('..') && !relativePath.startsWith('node_modules/');
}

View File

@ -28,13 +28,13 @@ export class ClusterExecutor implements Executor {
async execute(analyzeEntryPoints: AnalyzeEntryPointsFn, _createCompileFn: CreateCompileFn):
Promise<void> {
return this.lockFile.lock(() => {
return this.lockFile.lock(async () => {
this.logger.debug(
`Running ngcc on ${this.constructor.name} (using ${this.workerCount} worker processes).`);
const master = new ClusterMaster(
this.workerCount, this.fileSystem, this.logger, this.fileWriter, this.pkgJsonUpdater,
analyzeEntryPoints, this.createTaskCompletedCallback);
return master.run();
return await master.run();
});
}
}

View File

@ -7,6 +7,7 @@
*/
import * as ts from 'typescript';
import {absoluteFromSourceFile} from '../../../src/ngtsc/file_system';
import {ClassDeclaration, ClassMember, ClassMemberKind, CtorParameter, Declaration, Decorator, EnumMember, isDecoratorIdentifier, isNamedClassDeclaration, isNamedFunctionDeclaration, isNamedVariableDeclaration, KnownDeclaration, reflectObjectLiteral, SpecialDeclarationKind, TypeScriptReflectionHost, TypeValueReference} from '../../../src/ngtsc/reflection';
import {isWithinPackage} from '../analysis/util';
@ -2525,7 +2526,7 @@ function getRootFileOrFail(bundle: BundleProgram): ts.SourceFile {
function getNonRootPackageFiles(bundle: BundleProgram): ts.SourceFile[] {
const rootFile = bundle.program.getSourceFile(bundle.path);
return bundle.program.getSourceFiles().filter(
f => (f !== rootFile) && isWithinPackage(bundle.package, f));
f => (f !== rootFile) && isWithinPackage(bundle.package, absoluteFromSourceFile(f)));
}
function isTopLevel(node: ts.Node): boolean {

View File

@ -37,7 +37,11 @@ export class AsyncLocker {
*/
async lock<T>(fn: () => Promise<T>): Promise<T> {
await this.create();
return fn().finally(() => this.lockFile.remove());
try {
return await fn();
} finally {
this.lockFile.remove();
}
}
protected async create() {

View File

@ -50,7 +50,7 @@ export function makeEntryPointBundle(
const rootDir = entryPoint.packagePath;
const options: ts
.CompilerOptions = {allowJs: true, maxNodeModuleJsDepth: Infinity, rootDir, ...pathMappings};
const srcHost = new NgccSourcesCompilerHost(fs, options, entryPoint.path);
const srcHost = new NgccSourcesCompilerHost(fs, options, entryPoint.packagePath);
const dtsHost = new NgtscCompilerHost(fs, options);
// Create the bundle programs, as necessary.
@ -63,7 +63,7 @@ export function makeEntryPointBundle(
[];
const dts = transformDts ? makeBundleProgram(
fs, isCore, entryPoint.packagePath, typingsPath, 'r3_symbols.d.ts',
options, dtsHost, additionalDtsFiles) :
{...options, allowJs: false}, dtsHost, additionalDtsFiles) :
null;
const isFlatCore = isCore && src.r3SymbolsFile === null;

View File

@ -7,7 +7,8 @@
*/
import * as ts from 'typescript';
import {FileSystem, NgtscCompilerHost} from '../../../src/ngtsc/file_system';
import {AbsoluteFsPath, FileSystem, NgtscCompilerHost} from '../../../src/ngtsc/file_system';
import {isWithinPackage} from '../analysis/util';
import {isRelativePath} from '../utils';
/**
@ -20,7 +21,7 @@ export class NgccSourcesCompilerHost extends NgtscCompilerHost {
private cache = ts.createModuleResolutionCache(
this.getCurrentDirectory(), file => this.getCanonicalFileName(file));
constructor(fs: FileSystem, options: ts.CompilerOptions, protected entryPointPath: string) {
constructor(fs: FileSystem, options: ts.CompilerOptions, protected packagePath: AbsoluteFsPath) {
super(fs, options);
}
@ -36,13 +37,24 @@ export class NgccSourcesCompilerHost extends NgtscCompilerHost {
// file was in the same directory. This is undesirable, as we need to have the actual
// JavaScript being present in the program. This logic recognizes this scenario and rewrites
// the resolved .d.ts declaration file to its .js counterpart, if it exists.
if (resolvedModule !== undefined && resolvedModule.extension === ts.Extension.Dts &&
containingFile.endsWith('.js') && isRelativePath(moduleName)) {
if (resolvedModule?.extension === ts.Extension.Dts && containingFile.endsWith('.js') &&
isRelativePath(moduleName)) {
const jsFile = resolvedModule.resolvedFileName.replace(/\.d\.ts$/, '.js');
if (this.fileExists(jsFile)) {
return {...resolvedModule, resolvedFileName: jsFile, extension: ts.Extension.Js};
}
}
// Prevent loading JavaScript source files outside of the package root, which would happen for
// packages that don't have .d.ts files. As ngcc should only operate on the .js files
// contained within the package, any files outside the package are simply discarded. This does
// result in a partial program with error diagnostics, however ngcc won't gather diagnostics
// for the program it creates so these diagnostics won't be reported.
if (resolvedModule?.extension === ts.Extension.Js &&
!isWithinPackage(this.packagePath, this.fs.resolve(resolvedModule.resolvedFileName))) {
return undefined;
}
return resolvedModule;
});
}

View File

@ -5,7 +5,6 @@
* 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 * as ts from 'typescript';
import {absoluteFrom} from '../../../src/ngtsc/file_system';
import {runInEachFileSystem} from '../../../src/ngtsc/file_system/testing';
import {isWithinPackage} from '../../src/analysis/util';
@ -18,15 +17,13 @@ runInEachFileSystem(() => {
it('should return true if the source-file is contained in the package', () => {
const packagePath = _('/node_modules/test');
const file =
ts.createSourceFile(_('/node_modules/test/src/index.js'), '', ts.ScriptTarget.ES2015);
const file = _('/node_modules/test/src/index.js');
expect(isWithinPackage(packagePath, file)).toBe(true);
});
it('should return false if the source-file is not contained in the package', () => {
const packagePath = _('/node_modules/test');
const file =
ts.createSourceFile(_('/node_modules/other/src/index.js'), '', ts.ScriptTarget.ES2015);
const file = _('/node_modules/other/src/index.js');
expect(isWithinPackage(packagePath, file)).toBe(false);
});
@ -34,13 +31,11 @@ runInEachFileSystem(() => {
const packagePath = _('/node_modules/test');
// An external file inside the package's `node_modules/`.
const file1 = ts.createSourceFile(
_('/node_modules/test/node_modules/other/src/index.js'), '', ts.ScriptTarget.ES2015);
const file1 = _('/node_modules/test/node_modules/other/src/index.js');
expect(isWithinPackage(packagePath, file1)).toBe(false);
// An internal file starting with `node_modules`.
const file2 = ts.createSourceFile(
_('/node_modules/test/node_modules_optimizer.js'), '', ts.ScriptTarget.ES2015);
const file2 = _('/node_modules/test/node_modules_optimizer.js');
expect(isWithinPackage(packagePath, file2)).toBe(true);
});
});

View File

@ -34,7 +34,7 @@ runInEachFileSystem(() => {
beforeEach(() => {
masterRunSpy = spyOn(ClusterMaster.prototype, 'run')
.and.returnValue(Promise.resolve('CusterMaster#run()' as any));
.and.returnValue(Promise.resolve('ClusterMaster#run()' as any));
createTaskCompletedCallback = jasmine.createSpy('createTaskCompletedCallback');
mockLogger = new MockLogger();
@ -63,7 +63,7 @@ runInEachFileSystem(() => {
const createCompilerFnSpy = jasmine.createSpy('createCompilerFn');
expect(await executor.execute(analyzeEntryPointsSpy, createCompilerFnSpy))
.toBe('CusterMaster#run()' as any);
.toBe('ClusterMaster#run()' as any);
expect(masterRunSpy).toHaveBeenCalledWith();
@ -78,6 +78,22 @@ runInEachFileSystem(() => {
expect(lockFileLog).toEqual(['write()', 'remove()']);
});
it('should call LockFile.write() and LockFile.remove() if analyzeFn fails', async () => {
const analyzeEntryPointsSpy =
jasmine.createSpy('analyzeEntryPoints').and.throwError('analyze error');
const createCompilerFnSpy = jasmine.createSpy('createCompilerFn');
let error = '';
try {
await executor.execute(analyzeEntryPointsSpy, createCompilerFnSpy);
} catch (e) {
error = e.message;
}
expect(analyzeEntryPointsSpy).toHaveBeenCalledWith();
expect(createCompilerFnSpy).not.toHaveBeenCalled();
expect(error).toEqual('analyze error');
expect(lockFileLog).toEqual(['write()', 'remove()']);
});
it('should call LockFile.write() and LockFile.remove() if master runner fails', async () => {
const anyFn: () => any = () => undefined;
masterRunSpy.and.returnValue(Promise.reject(new Error('master runner error')));

View File

@ -68,7 +68,7 @@ export function makeTestBundleProgram(
const rootDir = fs.dirname(entryPointPath);
const options: ts.CompilerOptions =
{allowJs: true, maxNodeModuleJsDepth: Infinity, checkJs: false, rootDir, rootDirs: [rootDir]};
const host = new NgccSourcesCompilerHost(fs, options, entryPointPath);
const host = new NgccSourcesCompilerHost(fs, options, rootDir);
return makeBundleProgram(
fs, isCore, rootDir, path, 'r3_symbols.js', options, host, additionalFiles);
}

View File

@ -401,6 +401,121 @@ runInEachFileSystem(() => {
expect(es5Contents).toContain('ɵngcc0.ɵɵtext(0, "a - b - 3 - 4")');
});
it('should not crash when scanning for ModuleWithProviders needs to evaluate code from an external package',
() => {
// Regression test for https://github.com/angular/angular/issues/37508
// During `ModuleWithProviders` analysis, return statements in methods are evaluated using
// the partial evaluator to identify whether they correspond with a `ModuleWithProviders`
// function. If an arbitrary method has a return statement that calls into an external
// module which doesn't have declaration files, ngcc would attempt to reflect on said
// module using the reflection host of the entry-point. This would crash in the case where
// e.g. the entry-point is UMD and the external module would be CommonJS, as the UMD
// reflection host would throw because it is unable to deal with CommonJS.
// Setup a non-TS package with CommonJS module format
loadTestFiles([
{
name: _(`/node_modules/identity/package.json`),
contents: `{"name": "identity", "main": "./index.js"}`,
},
{
name: _(`/node_modules/identity/index.js`),
contents: `
function identity(x) { return x; };
exports.identity = identity;
module.exports = identity;
`,
},
]);
// Setup an Angular entry-point with UMD module format that references an export of the
// CommonJS package.
loadTestFiles([
{
name: _('/node_modules/test-package/package.json'),
contents: '{"name": "test-package", "main": "./index.js", "typings": "./index.d.ts"}'
},
{
name: _('/node_modules/test-package/index.js'),
contents: `
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('identity')) :
typeof define === 'function' && define.amd ? define('test', ['exports', 'identity'], factory) :
(factory(global.test, global.identity));
}(this, (function (exports, identity) { 'use strict';
function Foo(x) {
// The below statement is analyzed for 'ModuleWithProviders', so is evaluated
// by ngcc. The reference into the non-TS CommonJS package used to crash ngcc.
return identity.identity(x);
}
exports.Foo = Foo;
})));
`
},
{
name: _('/node_modules/test-package/index.d.ts'),
contents: 'export declare class Foo { static doSomething(x: any): any; }'
},
{name: _('/node_modules/test-package/index.metadata.json'), contents: 'DUMMY DATA'},
]);
expect(() => mainNgcc({
basePath: '/node_modules',
targetEntryPointPath: 'test-package',
propertiesToConsider: ['main'],
}))
.not.toThrow();
});
it('should not be able to evaluate code in external packages when no .d.ts files are present',
() => {
loadTestFiles([
{
name: _(`/node_modules/external/package.json`),
contents: `{"name": "external", "main": "./index.js"}`,
},
{
name: _(`/node_modules/external/index.js`),
contents: `
export const selector = 'my-selector';
`,
},
]);
compileIntoApf('test-package', {
'/index.ts': `
import {NgModule, Component} from '@angular/core';
import {selector} from 'external';
@Component({
selector,
template: ''
})
export class FooComponent {
}
@NgModule({
declarations: [FooComponent],
})
export class FooModule {}
`,
});
try {
mainNgcc({
basePath: '/node_modules',
targetEntryPointPath: 'test-package',
propertiesToConsider: ['esm2015', 'esm5'],
});
fail('should have thrown');
} catch (e) {
expect(e.message).toContain(
'Failed to compile entry-point test-package (esm2015 as esm2015) due to compilation errors:');
expect(e.message).toContain('NG1010');
expect(e.message).toContain('selector must be a string');
}
});
it('should add ɵfac but not duplicate ɵprov properties on injectables', () => {
compileIntoFlatEs2015Package('test-package', {
'/index.ts': `

View File

@ -13,8 +13,12 @@ import {makeEntryPointBundle} from '../../src/packages/entry_point_bundle';
runInEachFileSystem(() => {
describe('entry point bundle', () => {
let _: typeof absoluteFrom;
beforeEach(() => {
_ = absoluteFrom;
});
function setupMockFileSystem(): void {
const _ = absoluteFrom;
loadTestFiles([
{
name: _('/node_modules/test/package.json'),
@ -210,6 +214,103 @@ runInEachFileSystem(() => {
].map(p => absoluteFrom(p).toString())));
});
it('does not include .js files outside of the package when no .d.ts file is available', () => {
// Declare main "test" package with "entry" entry-point that imports all sorts of
// internal and external modules.
loadTestFiles([
{
name: _('/node_modules/test/entry/package.json'),
contents: `{"name": "test", "main": "./index.js", "typings": "./index.d.ts"}`,
},
{
name: _('/node_modules/test/entry/index.d.ts'),
contents: `
import 'external-js';
import 'external-ts';
import 'nested-js';
import './local';
import '../package';
`,
},
{
name: _('/node_modules/test/entry/index.js'),
contents: `
import 'external-js';
import 'external-ts';
import 'nested-js';
import './local';
import '../package';
`,
},
{name: _('/node_modules/test/entry/local.d.ts'), contents: `export {};`},
{name: _('/node_modules/test/entry/local.js'), contents: `export {};`},
{name: _('/node_modules/test/package.d.ts'), contents: `export {};`},
{name: _('/node_modules/test/package.js'), contents: `export {};`},
]);
// Declare "external-js" package outside of the "test" package without .d.ts files, should
// not be included in the program.
loadTestFiles([
{
name: _('/node_modules/external-js/package.json'),
contents: `{"name": "external-js", "main": "./index.js"}`,
},
{name: _('/node_modules/external-js/index.js'), contents: 'export {};'},
]);
// Same as "external-js" but located in a nested node_modules directory, which should also
// not be included in the program.
loadTestFiles([
{
name: _('/node_modules/test/node_modules/nested-js/package.json'),
contents: `{"name": "nested-js", "main": "./index.js"}`,
},
{name: _('/node_modules/test/node_modules/nested-js/index.js'), contents: 'export {}'},
]);
// Declare "external-ts" which does have .d.ts files, so the .d.ts should be
// loaded into the program.
loadTestFiles([
{
name: _('/node_modules/external-ts/package.json'),
contents: `{"name": "external-ts", "main": "./index.js", "typings": "./index.d.ts"}`,
},
{name: _('/node_modules/external-ts/index.d.ts'), contents: 'export {};'},
{name: _('/node_modules/external-ts/index.js'), contents: 'export {};'},
]);
const fs = getFileSystem();
const entryPoint: EntryPoint = {
name: 'test/entry',
path: absoluteFrom('/node_modules/test/entry'),
packageName: 'test',
packagePath: absoluteFrom('/node_modules/test'),
packageJson: {name: 'test/entry'},
typings: absoluteFrom('/node_modules/test/entry/index.d.ts'),
compiledByAngular: true,
ignoreMissingDependencies: false,
generateDeepReexports: false,
};
const esm5bundle = makeEntryPointBundle(
fs, entryPoint, './index.js', false, 'esm5', /* transformDts */ true,
/* pathMappings */ undefined, /* mirrorDtsFromSrc */ true);
expect(esm5bundle.src.program.getSourceFiles().map(sf => _(sf.fileName)))
.toEqual(jasmine.arrayWithExactContents([
_('/node_modules/test/entry/index.js'),
_('/node_modules/test/entry/local.js'),
_('/node_modules/test/package.js'),
_('/node_modules/external-ts/index.d.ts'),
]));
expect(esm5bundle.dts!.program.getSourceFiles().map(sf => _(sf.fileName)))
.toEqual(jasmine.arrayWithExactContents([
_('/node_modules/test/entry/index.d.ts'),
_('/node_modules/test/entry/local.d.ts'),
_('/node_modules/test/package.d.ts'),
_('/node_modules/external-ts/index.d.ts'),
]));
});
describe(
'including equivalently named, internally imported, src files in the typings program',
() => {

View File

@ -116,7 +116,13 @@ export class NgCompiler {
const moduleResolutionCache = ts.createModuleResolutionCache(
this.adapter.getCurrentDirectory(),
fileName => this.adapter.getCanonicalFileName(fileName));
// Note: this used to be an arrow-function closure. However, JS engines like v8 have some
// strange behaviors with retaining the lexical scope of the closure. Even if this function
// doesn't retain a reference to `this`, if other closures in the constructor here reference
// `this` internally then a closure created here would retain them. This can cause major
// memory leak issues since the `moduleResolutionCache` is a long-lived object and finds its
// way into all kinds of places inside TS internal objects.
this.adapter.getCanonicalFileName.bind(this.adapter));
this.moduleResolver =
new ModuleResolver(tsProgram, this.options, this.adapter, moduleResolutionCache);
this.resourceManager = new AdapterResourceLoader(adapter, this.options);

View File

@ -42,20 +42,22 @@ export class NoopIncrementalBuildStrategy implements IncrementalBuildStrategy {
* Tracks an `IncrementalDriver` within the strategy itself.
*/
export class TrackedIncrementalBuildStrategy implements IncrementalBuildStrategy {
private previous: IncrementalDriver|null = null;
private next: IncrementalDriver|null = null;
private driver: IncrementalDriver|null = null;
private isSet: boolean = false;
getIncrementalDriver(): IncrementalDriver|null {
return this.next !== null ? this.next : this.previous;
return this.driver;
}
setIncrementalDriver(driver: IncrementalDriver): void {
this.next = driver;
this.driver = driver;
this.isSet = true;
}
toNextBuildStrategy(): TrackedIncrementalBuildStrategy {
const strategy = new TrackedIncrementalBuildStrategy();
strategy.previous = this.next;
// Only reuse a driver that was explicitly set via `setIncrementalDriver`.
strategy.driver = this.isSet ? this.driver : null;
return strategy;
}
}

View File

@ -17,6 +17,7 @@ import {NgCompilerOptions} from './core/api';
import {TrackedIncrementalBuildStrategy} from './incremental';
import {IndexedComponent} from './indexer';
import {NOOP_PERF_RECORDER, PerfRecorder, PerfTracker} from './perf';
import {retagAllTsFiles, untagAllTsFiles} from './shims';
import {ReusedProgramStrategy} from './typecheck';
@ -68,14 +69,26 @@ export class NgtscProgram implements api.Program {
}
this.closureCompilerEnabled = !!options.annotateForClosureCompiler;
const reuseProgram = oldProgram && oldProgram.reuseTsProgram;
const reuseProgram = oldProgram?.reuseTsProgram;
this.host = NgCompilerHost.wrap(delegateHost, rootNames, options, reuseProgram ?? null);
if (reuseProgram !== undefined) {
// Prior to reusing the old program, restore shim tagging for all its `ts.SourceFile`s.
// TypeScript checks the `referencedFiles` of `ts.SourceFile`s for changes when evaluating
// incremental reuse of data from the old program, so it's important that these match in order
// to get the most benefit out of reuse.
retagAllTsFiles(reuseProgram);
}
this.tsProgram = ts.createProgram(this.host.inputFiles, options, this.host, reuseProgram);
this.reuseTsProgram = this.tsProgram;
this.host.postProgramCreationCleanup();
// Shim tagging has served its purpose, and tags can now be removed from all `ts.SourceFile`s in
// the program.
untagAllTsFiles(this.tsProgram);
const reusedProgramStrategy = new ReusedProgramStrategy(
this.tsProgram, this.host, this.options, this.host.shimExtensionPrefixes);
@ -93,6 +106,10 @@ export class NgtscProgram implements api.Program {
return this.tsProgram;
}
getReuseTsProgram(): ts.Program {
return this.reuseTsProgram;
}
getTsOptionDiagnostics(cancellationToken?: ts.CancellationToken|
undefined): readonly ts.Diagnostic[] {
return this.tsProgram.getOptionsDiagnostics(cancellationToken);
@ -248,6 +265,7 @@ export class NgtscProgram implements api.Program {
}));
this.perfRecorder.stop(fileEmitSpan);
}
this.perfRecorder.stop(emitSpan);
if (this.perfTracker !== null && this.options.tracePerformance !== undefined) {

View File

@ -9,7 +9,7 @@
/// <reference types="node" />
export {ShimAdapter} from './src/adapter';
export {copyFileShimData, isShim} from './src/expando';
export {copyFileShimData, isShim, retagAllTsFiles, retagTsFile, sfExtensionData, untagAllTsFiles, untagTsFile} from './src/expando';
export {FactoryGenerator, generatedFactoryTransform} from './src/factory_generator';
export {ShimReferenceTagger} from './src/reference_tagger';
export {SummaryGenerator} from './src/summary_generator';

View File

@ -12,7 +12,7 @@ import {absoluteFrom, absoluteFromSourceFile, AbsoluteFsPath} from '../../file_s
import {isDtsPath} from '../../util/src/typescript';
import {PerFileShimGenerator, TopLevelShimGenerator} from '../api';
import {isFileShimSourceFile, isShim, NgExtension, sfExtensionData} from './expando';
import {isFileShimSourceFile, isShim, sfExtensionData} from './expando';
import {makeShimFileName} from './util';
interface ShimGeneratorData {

View File

@ -21,7 +21,16 @@ export const NgExtension = Symbol('NgExtension');
export interface NgExtensionData {
isTopLevelShim: boolean;
fileShim: NgFileShimData|null;
/**
* The contents of the `referencedFiles` array, before modification by a `ShimReferenceTagger`.
*/
originalReferencedFiles: ReadonlyArray<ts.FileReference>|null;
/**
* The contents of the `referencedFiles` array, after modification by a `ShimReferenceTagger`.
*/
taggedReferenceFiles: ReadonlyArray<ts.FileReference>|null;
}
/**
@ -65,6 +74,7 @@ export function sfExtensionData(sf: ts.SourceFile): NgExtensionData {
isTopLevelShim: false,
fileShim: null,
originalReferencedFiles: null,
taggedReferenceFiles: null,
};
extSf[NgExtension] = extension;
return extension;
@ -110,3 +120,53 @@ export function copyFileShimData(from: ts.SourceFile, to: ts.SourceFile): void {
}
sfExtensionData(to).fileShim = sfExtensionData(from).fileShim;
}
/**
* For those `ts.SourceFile`s in the `program` which have previously been tagged by a
* `ShimReferenceTagger`, restore the original `referencedFiles` array that does not have shim tags.
*/
export function untagAllTsFiles(program: ts.Program): void {
for (const sf of program.getSourceFiles()) {
untagTsFile(sf);
}
}
/**
* For those `ts.SourceFile`s in the `program` which have previously been tagged by a
* `ShimReferenceTagger`, re-apply the effects of tagging by updating the `referencedFiles` array to
* the tagged version produced previously.
*/
export function retagAllTsFiles(program: ts.Program): void {
for (const sf of program.getSourceFiles()) {
retagTsFile(sf);
}
}
/**
* Restore the original `referencedFiles` for the given `ts.SourceFile`.
*/
export function untagTsFile(sf: ts.SourceFile): void {
if (sf.isDeclarationFile || !isExtended(sf)) {
return;
}
const ext = sfExtensionData(sf);
if (ext.originalReferencedFiles !== null) {
sf.referencedFiles = ext.originalReferencedFiles as Array<ts.FileReference>;
}
}
/**
* Apply the previously tagged `referencedFiles` to the given `ts.SourceFile`, if it was previously
* tagged.
*/
export function retagTsFile(sf: ts.SourceFile): void {
if (sf.isDeclarationFile || !isExtended(sf)) {
return;
}
const ext = sfExtensionData(sf);
if (ext.taggedReferenceFiles !== null) {
sf.referencedFiles = ext.taggedReferenceFiles as Array<ts.FileReference>;
}
}

View File

@ -8,10 +8,10 @@
import * as ts from 'typescript';
import {absoluteFrom, absoluteFromSourceFile} from '../../file_system';
import {absoluteFromSourceFile} from '../../file_system';
import {isNonDeclarationTsPath} from '../../util/src/typescript';
import {isExtended as isExtendedSf, isShim, NgExtension, sfExtensionData} from './expando';
import {isShim, sfExtensionData} from './expando';
import {makeShimFileName} from './util';
/**
@ -48,8 +48,16 @@ export class ShimReferenceTagger {
return;
}
sfExtensionData(sf).originalReferencedFiles = sf.referencedFiles;
const referencedFiles = [...sf.referencedFiles];
const ext = sfExtensionData(sf);
// If this file has never been tagged before, capture its `referencedFiles` in the extension
// data.
if (ext.originalReferencedFiles === null) {
ext.originalReferencedFiles = sf.referencedFiles;
}
const referencedFiles = [...ext.originalReferencedFiles];
const sfPath = absoluteFromSourceFile(sf);
for (const suffix of this.suffixes) {
@ -60,26 +68,16 @@ export class ShimReferenceTagger {
});
}
ext.taggedReferenceFiles = referencedFiles;
sf.referencedFiles = referencedFiles;
this.tagged.add(sf);
}
/**
* Restore the original `referencedFiles` values of all tagged `ts.SourceFile`s and disable the
* `ShimReferenceTagger`.
* Disable the `ShimReferenceTagger` and free memory associated with tracking tagged files.
*/
finalize(): void {
this.enabled = false;
for (const sf of this.tagged) {
if (!isExtendedSf(sf)) {
continue;
}
const extensionData = sfExtensionData(sf);
if (extensionData.originalReferencedFiles !== null) {
sf.referencedFiles = extensionData.originalReferencedFiles! as ts.FileReference[];
}
}
this.tagged.clear();
}
}

View File

@ -12,6 +12,7 @@ import {absoluteFrom as _, AbsoluteFsPath, getSourceFileOrError} from '../../fil
import {runInEachFileSystem} from '../../file_system/testing';
import {makeProgram} from '../../testing';
import {ShimAdapter} from '../src/adapter';
import {retagTsFile, untagTsFile} from '../src/expando';
import {ShimReferenceTagger} from '../src/reference_tagger';
import {TestShimGenerator} from './util';
@ -67,40 +68,6 @@ runInEachFileSystem(() => {
expect(shimSf.referencedFiles).toEqual([]);
});
it('should remove tags during finalization', () => {
const tagger = new ShimReferenceTagger(['test1', 'test2']);
const fileName = _('/file.ts');
const sf = makeArbitrarySf(fileName);
expectReferencedFiles(sf, []);
tagger.tag(sf);
expectReferencedFiles(sf, ['/file.test1.ts', '/file.test2.ts']);
tagger.finalize();
expectReferencedFiles(sf, []);
});
it('should not remove references it did not add during finalization', () => {
const tagger = new ShimReferenceTagger(['test1', 'test2']);
const fileName = _('/file.ts');
const libFileName = _('/lib.d.ts');
const sf = makeSf(fileName, `
/// <reference path="/lib.d.ts" />
export const UNIMPORTANT = true;
`);
expectReferencedFiles(sf, [libFileName]);
tagger.tag(sf);
expectReferencedFiles(sf, ['/file.test1.ts', '/file.test2.ts', libFileName]);
tagger.finalize();
expectReferencedFiles(sf, [libFileName]);
});
it('should not tag shims after finalization', () => {
const tagger = new ShimReferenceTagger(['test1', 'test2']);
tagger.finalize();
@ -111,6 +78,56 @@ runInEachFileSystem(() => {
tagger.tag(sf);
expectReferencedFiles(sf, []);
});
it('should not overwrite original referencedFiles', () => {
const tagger = new ShimReferenceTagger(['test']);
const fileName = _('/file.ts');
const sf = makeArbitrarySf(fileName);
sf.referencedFiles = [{
fileName: _('/other.ts'),
pos: 0,
end: 0,
}];
tagger.tag(sf);
expectReferencedFiles(sf, ['/other.ts', '/file.test.ts']);
});
it('should always tag against the original referencedFiles', () => {
const tagger1 = new ShimReferenceTagger(['test1']);
const tagger2 = new ShimReferenceTagger(['test2']);
const fileName = _('/file.ts');
const sf = makeArbitrarySf(fileName);
tagger1.tag(sf);
tagger2.tag(sf);
expectReferencedFiles(sf, ['/file.test2.ts']);
});
describe('tagging and untagging', () => {
it('should be able to untag references and retag them later', () => {
const tagger = new ShimReferenceTagger(['test']);
const fileName = _('/file.ts');
const sf = makeArbitrarySf(fileName);
sf.referencedFiles = [{
fileName: _('/other.ts'),
pos: 0,
end: 0,
}];
tagger.tag(sf);
expectReferencedFiles(sf, ['/other.ts', '/file.test.ts']);
untagTsFile(sf);
expectReferencedFiles(sf, ['/other.ts']);
retagTsFile(sf);
expectReferencedFiles(sf, ['/other.ts', '/file.test.ts']);
});
});
});
});

View File

@ -5,4 +5,4 @@
* 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 {getDeclaration, makeProgram} from './src/utils';
export {expectCompleteReuse, getDeclaration, makeProgram} from './src/utils';

View File

@ -97,6 +97,23 @@ export function walkForDeclaration(name: string, rootNode: ts.Node): ts.Declarat
return chosenDecl;
}
const COMPLETE_REUSE_FAILURE_MESSAGE =
'The original program was not reused completely, even though no changes should have been made to its structure';
/**
* Extracted from TypeScript's internal enum `StructureIsReused`.
*/
enum TsStructureIsReused {
Not = 0,
SafeModules = 1,
Completely = 2,
}
export function expectCompleteReuse(oldProgram: ts.Program): void {
// Assert complete reuse using TypeScript's private API.
expect((oldProgram as any).structureIsReused)
.toBe(TsStructureIsReused.Completely, COMPLETE_REUSE_FAILURE_MESSAGE);
}
function bindingNameEquals(node: ts.BindingName, name: string): boolean {
if (ts.isIdentifier(node)) {

View File

@ -13,6 +13,7 @@ import {NgCompilerOptions, UnifiedModulesHost} from './core/api';
import {NodeJSFileSystem, setFileSystem} from './file_system';
import {PatchedProgramIncrementalBuildStrategy} from './incremental';
import {NOOP_PERF_RECORDER} from './perf';
import {untagAllTsFiles} from './shims';
import {ReusedProgramStrategy} from './typecheck/src/augmented_program';
// The following is needed to fix a the chicken-and-egg issue where the sync (into g3) script will
@ -80,6 +81,9 @@ export class NgTscPlugin implements TscPlugin {
wrapHost(
host: ts.CompilerHost&UnifiedModulesHost, inputFiles: readonly string[],
options: ts.CompilerOptions): PluginCompilerHost {
// TODO(alxhub): Eventually the `wrapHost()` API will accept the old `ts.Program` (if one is
// available). When it does, its `ts.SourceFile`s need to be re-tagged to enable proper
// incremental compilation.
this.options = {...this.ngOptions, ...options} as NgCompilerOptions;
this.host = NgCompilerHost.wrap(host, inputFiles, this.options, /* oldProgram */ null);
return this.host;
@ -92,6 +96,8 @@ export class NgTscPlugin implements TscPlugin {
if (this.host === null || this.options === null) {
throw new Error('Lifecycle error: setupCompilation() before wrapHost().');
}
this.host.postProgramCreationCleanup();
untagAllTsFiles(program);
const typeCheckStrategy = new ReusedProgramStrategy(
program, this.host, this.options, this.host.shimExtensionPrefixes);
this._compiler = new NgCompiler(

View File

@ -9,6 +9,7 @@
import * as ts from 'typescript';
import {AbsoluteFsPath} from '../../file_system';
import {retagAllTsFiles, untagAllTsFiles} from '../../shims';
import {TypeCheckingProgramStrategy, UpdateMode} from './api';
import {TypeCheckProgramHost} from './host';
@ -26,8 +27,10 @@ export class ReusedProgramStrategy implements TypeCheckingProgramStrategy {
*/
private sfMap = new Map<string, ts.SourceFile>();
private program: ts.Program = this.originalProgram;
constructor(
private program: ts.Program, private originalHost: ts.CompilerHost,
private originalProgram: ts.Program, private originalHost: ts.CompilerHost,
private options: ts.CompilerOptions, private shimExtensionPrefixes: string[]) {}
getProgram(): ts.Program {
@ -35,6 +38,17 @@ export class ReusedProgramStrategy implements TypeCheckingProgramStrategy {
}
updateFiles(contents: Map<AbsoluteFsPath, string>, updateMode: UpdateMode): void {
if (contents.size === 0) {
// No changes have been requested. Is it safe to skip updating entirely?
// If UpdateMode is Incremental, then yes. If UpdateMode is Complete, then it's safe to skip
// only if there are no active changes already (that would be cleared by the update).
if (updateMode !== UpdateMode.Complete || this.sfMap.size === 0) {
// No changes would be made to the `ts.Program` anyway, so it's safe to do nothing here.
return;
}
}
if (updateMode === UpdateMode.Complete) {
this.sfMap.clear();
}
@ -43,14 +57,25 @@ export class ReusedProgramStrategy implements TypeCheckingProgramStrategy {
this.sfMap.set(filePath, ts.createSourceFile(filePath, text, ts.ScriptTarget.Latest, true));
}
const host =
new TypeCheckProgramHost(this.sfMap, this.originalHost, this.shimExtensionPrefixes);
const host = new TypeCheckProgramHost(
this.sfMap, this.originalProgram, this.originalHost, this.shimExtensionPrefixes);
const oldProgram = this.program;
// Retag the old program's `ts.SourceFile`s with shim tags, to allow TypeScript to reuse the
// most data.
retagAllTsFiles(oldProgram);
this.program = ts.createProgram({
host,
rootNames: this.program.getRootFileNames(),
options: this.options,
oldProgram: this.program,
oldProgram,
});
host.postProgramCreationCleanup();
// And untag them afterwards. We explicitly untag both programs here, because the oldProgram
// may still be used for emit and needs to not contain tags.
untagAllTsFiles(this.program);
untagAllTsFiles(oldProgram);
}
}

View File

@ -35,8 +35,8 @@ export class TypeCheckProgramHost implements ts.CompilerHost {
readonly resolveModuleNames?: ts.CompilerHost['resolveModuleNames'];
constructor(
sfMap: Map<string, ts.SourceFile>, private delegate: ts.CompilerHost,
private shimExtensionPrefixes: string[]) {
sfMap: Map<string, ts.SourceFile>, private originalProgram: ts.Program,
private delegate: ts.CompilerHost, private shimExtensionPrefixes: string[]) {
this.sfMap = sfMap;
if (delegate.getDirectories !== undefined) {
@ -52,8 +52,15 @@ export class TypeCheckProgramHost implements ts.CompilerHost {
fileName: string, languageVersion: ts.ScriptTarget,
onError?: ((message: string) => void)|undefined,
shouldCreateNewSourceFile?: boolean|undefined): ts.SourceFile|undefined {
const delegateSf =
this.delegate.getSourceFile(fileName, languageVersion, onError, shouldCreateNewSourceFile)!;
// Try to use the same `ts.SourceFile` as the original program, if possible. This guarantees
// that program reuse will be as efficient as possible.
let delegateSf: ts.SourceFile|undefined = this.originalProgram.getSourceFile(fileName);
if (delegateSf === undefined) {
// Something went wrong and a source file is being requested that's not in the original
// program. Just in case, try to retrieve it from the delegate.
delegateSf = this.delegate.getSourceFile(
fileName, languageVersion, onError, shouldCreateNewSourceFile)!;
}
if (delegateSf === undefined) {
return undefined;
}

View File

@ -16,6 +16,7 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/imports",
"//packages/compiler-cli/src/ngtsc/incremental",
"//packages/compiler-cli/src/ngtsc/reflection",
"//packages/compiler-cli/src/ngtsc/shims",
"//packages/compiler-cli/src/ngtsc/testing",
"//packages/compiler-cli/src/ngtsc/typecheck",
"//packages/compiler-cli/src/ngtsc/util",

View File

@ -0,0 +1,98 @@
/**
* @license
* Copyright Google LLC 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 * as ts from 'typescript';
import {absoluteFrom, AbsoluteFsPath, getSourceFileOrError} from '../../file_system';
import {runInEachFileSystem} from '../../file_system/testing';
import {sfExtensionData, ShimReferenceTagger} from '../../shims';
import {expectCompleteReuse, makeProgram} from '../../testing';
import {UpdateMode} from '../src/api';
import {ReusedProgramStrategy} from '../src/augmented_program';
import {createProgramWithNoTemplates} from './test_utils';
runInEachFileSystem(() => {
describe('template type-checking program', () => {
it('should not be created if no components need to be checked', () => {
const {program, templateTypeChecker, programStrategy} = createProgramWithNoTemplates();
templateTypeChecker.refresh();
// expect() here would create a really long error message, so this is checked manually.
if (programStrategy.getProgram() !== program) {
fail('Template type-checking created a new ts.Program even though it had no changes.');
}
});
it('should have complete reuse if no structural changes are made to shims', () => {
const {program, host, options, typecheckPath} = makeSingleFileProgramWithTypecheckShim();
const programStrategy = new ReusedProgramStrategy(program, host, options, ['ngtypecheck']);
// Update /main.ngtypecheck.ts without changing its shape. Verify that the old program was
// reused completely.
programStrategy.updateFiles(
new Map([[typecheckPath, 'export const VERSION = 2;']]), UpdateMode.Complete);
expectCompleteReuse(program);
});
it('should have complete reuse if no structural changes are made to input files', () => {
const {program, host, options, mainPath} = makeSingleFileProgramWithTypecheckShim();
const programStrategy = new ReusedProgramStrategy(program, host, options, ['ngtypecheck']);
// Update /main.ts without changing its shape. Verify that the old program was reused
// completely.
programStrategy.updateFiles(
new Map([[mainPath, 'export const STILL_NOT_A_COMPONENT = true;']]), UpdateMode.Complete);
expectCompleteReuse(program);
});
});
});
function makeSingleFileProgramWithTypecheckShim(): {
program: ts.Program,
host: ts.CompilerHost,
options: ts.CompilerOptions,
mainPath: AbsoluteFsPath,
typecheckPath: AbsoluteFsPath,
} {
const mainPath = absoluteFrom('/main.ts');
const typecheckPath = absoluteFrom('/main.ngtypecheck.ts');
const {program, host, options} = makeProgram([
{
name: mainPath,
contents: 'export const NOT_A_COMPONENT = true;',
},
{
name: typecheckPath,
contents: 'export const VERSION = 1;',
}
]);
const sf = getSourceFileOrError(program, mainPath);
const typecheckSf = getSourceFileOrError(program, typecheckPath);
// To ensure this test is validating the correct behavior, the initial conditions of the
// input program must be such that:
//
// 1) /main.ts was previously tagged with a reference to its ngtypecheck shim.
// 2) /main.ngtypecheck.ts is marked as a shim itself.
// Condition 1:
const tagger = new ShimReferenceTagger(['ngtypecheck']);
tagger.tag(sf);
tagger.finalize();
// Condition 2:
sfExtensionData(typecheckSf).fileShim = {
extension: 'ngtypecheck',
generatedFrom: mainPath,
};
return {program, host, options, mainPath, typecheckPath};
}

View File

@ -235,10 +235,18 @@ export function tcb(
return res.replace(/\s+/g, ' ');
}
export function typecheck(
template: string, source: string, declarations: TestDeclaration[] = [],
additionalSources: {name: AbsoluteFsPath; contents: string}[] = [],
config: Partial<TypeCheckingConfig> = {}, opts: ts.CompilerOptions = {}): ts.Diagnostic[] {
export interface TemplateTestEnvironment {
sf: ts.SourceFile;
program: ts.Program;
templateTypeChecker: TemplateTypeChecker;
programStrategy: ReusedProgramStrategy;
}
function setupTemplateTypeChecking(
source: string, additionalSources: {name: AbsoluteFsPath; contents: string}[],
config: Partial<TypeCheckingConfig>, opts: ts.CompilerOptions,
makeTypeCheckAdapterFn: (program: ts.Program, sf: ts.SourceFile) =>
ProgramTypeCheckAdapter): TemplateTestEnvironment {
const typeCheckFilePath = absoluteFrom('/main.ngtypecheck.ts');
const files = [
typescriptLibDts(),
@ -266,48 +274,65 @@ export function typecheck(
]);
const fullConfig = {...ALL_ENABLED_CONFIG, ...config};
const templateUrl = 'synthetic.html';
const templateFile = new ParseSourceFile(template, templateUrl);
const {nodes, errors} = parseTemplate(template, templateUrl);
if (errors !== undefined) {
throw new Error('Template parse errors: \n' + errors.join('\n'));
}
const {matcher, pipes} = prepareDeclarations(declarations, decl => {
let declFile = sf;
if (decl.file !== undefined) {
declFile = program.getSourceFile(decl.file)!;
if (declFile === undefined) {
throw new Error(`Unable to locate ${decl.file} for ${decl.type} ${decl.name}`);
}
}
return getClass(declFile, decl.name);
});
const binder = new R3TargetBinder(matcher);
const boundTarget = binder.bind({template: nodes});
const clazz = new Reference(getClass(sf, 'TestComponent'));
const sourceMapping: TemplateSourceMapping = {
type: 'external',
template,
templateUrl,
componentClass: clazz.node,
// Use the class's name for error mappings.
node: clazz.node.name,
};
const checkAdapter = createTypeCheckAdapter((ctx: TypeCheckContext) => {
ctx.addTemplate(clazz, boundTarget, pipes, [], sourceMapping, templateFile);
});
const checkAdapter = makeTypeCheckAdapterFn(program, sf);
const programStrategy = new ReusedProgramStrategy(program, host, options, []);
const templateTypeChecker = new TemplateTypeChecker(
program, programStrategy, checkAdapter, fullConfig, emitter, reflectionHost, host,
NOOP_INCREMENTAL_BUILD);
return {program, sf, templateTypeChecker, programStrategy};
}
export function typecheck(
template: string, source: string, declarations: TestDeclaration[] = [],
additionalSources: {name: AbsoluteFsPath; contents: string}[] = [],
config: Partial<TypeCheckingConfig> = {}, opts: ts.CompilerOptions = {}): ts.Diagnostic[] {
const {sf, templateTypeChecker} =
setupTemplateTypeChecking(source, additionalSources, config, opts, (program, sf) => {
const templateUrl = 'synthetic.html';
const templateFile = new ParseSourceFile(template, templateUrl);
const {nodes, errors} = parseTemplate(template, templateUrl);
if (errors !== undefined) {
throw new Error('Template parse errors: \n' + errors.join('\n'));
}
const {matcher, pipes} = prepareDeclarations(declarations, decl => {
let declFile = sf;
if (decl.file !== undefined) {
declFile = program.getSourceFile(decl.file)!;
if (declFile === undefined) {
throw new Error(`Unable to locate ${decl.file} for ${decl.type} ${decl.name}`);
}
}
return getClass(declFile, decl.name);
});
const binder = new R3TargetBinder(matcher);
const boundTarget = binder.bind({template: nodes});
const clazz = new Reference(getClass(sf, 'TestComponent'));
const sourceMapping: TemplateSourceMapping = {
type: 'external',
template,
templateUrl,
componentClass: clazz.node,
// Use the class's name for error mappings.
node: clazz.node.name,
};
return createTypeCheckAdapter((ctx: TypeCheckContext) => {
ctx.addTemplate(clazz, boundTarget, pipes, [], sourceMapping, templateFile);
});
});
templateTypeChecker.refresh();
return templateTypeChecker.getDiagnosticsForFile(sf);
}
export function createProgramWithNoTemplates(): TemplateTestEnvironment {
return setupTemplateTypeChecking(
'export const NOT_A_COMPONENT = true;', [], {}, {}, () => createTypeCheckAdapter(() => {}));
}
function createTypeCheckAdapter(fn: (ctx: TypeCheckContext) => void): ProgramTypeCheckAdapter {
let called = false;
return {

View File

@ -125,10 +125,11 @@ export function resolveModuleName(
compilerHost: ts.ModuleResolutionHost&Pick<ts.CompilerHost, 'resolveModuleNames'>,
moduleResolutionCache: ts.ModuleResolutionCache|null): ts.ResolvedModule|undefined {
if (compilerHost.resolveModuleNames) {
// FIXME: Additional parameters are required in TS3.6, but ignored in 3.5.
// Remove the any cast once google3 is fully on TS3.6.
return (compilerHost as any)
.resolveModuleNames([moduleName], containingFile, undefined, undefined, compilerOptions)[0];
return compilerHost.resolveModuleNames(
[moduleName], containingFile,
undefined, // reusedNames
undefined, // redirectedReference
compilerOptions)[0];
} else {
return ts
.resolveModuleName(

View File

@ -12,6 +12,7 @@ ts_library(
"//packages/compiler-cli/src/ngtsc/file_system/testing",
"//packages/compiler-cli/src/ngtsc/indexer",
"//packages/compiler-cli/src/ngtsc/routing",
"//packages/compiler-cli/src/ngtsc/testing",
"//packages/compiler-cli/src/ngtsc/util",
"//packages/compiler-cli/test:test_utils",
"//packages/compiler-cli/test/helpers",

View File

@ -128,6 +128,13 @@ export class NgtscTestEnvironment {
return this.oldProgram.getTsProgram();
}
getReuseTsProgram(): ts.Program {
if (this.oldProgram === null) {
throw new Error('No ts.Program has been created yet.');
}
return (this.oldProgram as NgtscProgram).getReuseTsProgram();
}
/**
* Older versions of the CLI do not provide the `CompilerHost.getModifiedResourceFiles()` method.
* This results in the `changedResources` set being `null`.

View File

@ -9,8 +9,9 @@
import * as ts from 'typescript';
import {ErrorCode, ngErrorCode} from '../../src/ngtsc/diagnostics';
import {absoluteFrom as _, getFileSystem} from '../../src/ngtsc/file_system';
import {absoluteFrom as _, getFileSystem, getSourceFileOrError} from '../../src/ngtsc/file_system';
import {runInEachFileSystem} from '../../src/ngtsc/file_system/testing';
import {expectCompleteReuse} from '../../src/ngtsc/testing';
import {loadStandardTestFiles} from '../helpers/src/mock_file_loading';
import {NgtscTestEnvironment} from './env';
@ -1862,18 +1863,26 @@ export declare class AnimationEvent {
expect(env.driveDiagnostics()).toEqual([]);
});
it('should not leave references to shims after execution', () => {
// This test verifies that proper cleanup is performed for the technique being used to
// include shim files in the ts.Program, and that none are left in the referencedFiles of
// any ts.SourceFile after compilation.
it('should not leave referencedFiles in a tagged state', () => {
env.enableMultipleCompilations();
env.driveMain();
for (const sf of env.getTsProgram().getSourceFiles()) {
for (const ref of sf.referencedFiles) {
expect(ref.fileName).not.toContain('.ngtypecheck.ts');
}
}
const sf = getSourceFileOrError(env.getTsProgram(), _('/test.ts'));
expect(sf.referencedFiles.map(ref => ref.fileName)).toEqual([]);
});
it('should allow for complete program reuse during incremental compilations', () => {
env.enableMultipleCompilations();
env.write('other.ts', `export const VERSION = 1;`);
env.driveMain();
const firstProgram = env.getReuseTsProgram();
env.write('other.ts', `export const VERSION = 2;`);
env.driveMain();
expectCompleteReuse(firstProgram);
});
});
});

View File

@ -43,20 +43,27 @@ const DIRECTIVE_LIFECYCLE_HOOKS = new Set([
const AMBIGUOUS_LIFECYCLE_HOOKS = new Set(['ngOnDestroy']);
/** Describes how a given class is used in the context of Angular. */
enum ClassKind {
enum InferredKind {
DIRECTIVE,
AMBIGUOUS,
UNKNOWN,
}
/** Describes possible types of Angular declarations. */
enum DeclarationType {
DIRECTIVE,
COMPONENT,
ABSTRACT_DIRECTIVE,
PIPE,
INJECTABLE,
}
/** Analyzed class declaration. */
interface AnalyzedClass {
/** Whether the class is decorated with @Directive or @Component. */
isDirectiveOrComponent: boolean;
/** Whether the class is an abstract directive. */
isAbstractDirective: boolean;
/** Kind of the given class in terms of Angular. */
kind: ClassKind;
/** Type of declaration that is determined through an applied decorator. */
decoratedType: DeclarationType|null;
/** Inferred class kind in terms of Angular. */
inferredKind: InferredKind;
}
interface AnalysisFailure {
@ -64,6 +71,9 @@ interface AnalysisFailure {
message: string;
}
/** TODO message that is added to ambiguous classes using Angular features. */
const AMBIGUOUS_CLASS_TODO = 'Add Angular decorator.';
export class UndecoratedClassesWithDecoratedFieldsTransform {
private printer = ts.createPrinter();
private importManager = new ImportManager(this.getUpdateRecorder, this.printer);
@ -81,10 +91,10 @@ export class UndecoratedClassesWithDecoratedFieldsTransform {
* indicating that a given class uses Angular features. https://hackmd.io/vuQfavzfRG6KUCtU7oK_EA
*/
migrate(sourceFiles: ts.SourceFile[]): AnalysisFailure[] {
const {result, ambiguous} = this._findUndecoratedAbstractDirectives(sourceFiles);
const {detectedAbstractDirectives, ambiguousClasses} =
this._findUndecoratedAbstractDirectives(sourceFiles);
result.forEach(node => {
detectedAbstractDirectives.forEach(node => {
const sourceFile = node.getSourceFile();
const recorder = this.getUpdateRecorder(sourceFile);
const directiveExpr =
@ -98,12 +108,19 @@ export class UndecoratedClassesWithDecoratedFieldsTransform {
// determine whether the class is used as directive, service or pipe. The migration
// could potentially determine the type by checking NgModule definitions or inheritance
// of other known declarations, but this is out of scope and a TODO/failure is sufficient.
return Array.from(ambiguous).reduce((failures, node) => {
return Array.from(ambiguousClasses).reduce((failures, node) => {
// If the class has been reported as ambiguous before, skip adding a TODO and
// printing an error. A class could be visited multiple times when it's part
// of multiple build targets in the CLI project.
if (this._hasBeenReportedAsAmbiguous(node)) {
return failures;
}
const sourceFile = node.getSourceFile();
const recorder = this.getUpdateRecorder(sourceFile);
// Add a TODO to the class that uses Angular features but is not decorated.
recorder.addClassTodo(node, `Add Angular decorator.`);
recorder.addClassTodo(node, AMBIGUOUS_CLASS_TODO);
// Add an error for the class that will be printed in the `ng update` output.
return failures.concat({
@ -125,59 +142,83 @@ export class UndecoratedClassesWithDecoratedFieldsTransform {
* directives. Those are ambiguous and could be either Directive, Pipe or service.
*/
private _findUndecoratedAbstractDirectives(sourceFiles: ts.SourceFile[]) {
const result = new Set<ts.ClassDeclaration>();
const ambiguousClasses = new Set<ts.ClassDeclaration>();
const declarations = new WeakMap<ts.ClassDeclaration, DeclarationType>();
const detectedAbstractDirectives = new Set<ts.ClassDeclaration>();
const undecoratedClasses = new Set<ts.ClassDeclaration>();
const nonAbstractDirectives = new WeakSet<ts.ClassDeclaration>();
const abstractDirectives = new WeakSet<ts.ClassDeclaration>();
const ambiguous = new Set<ts.ClassDeclaration>();
const visitNode = (node: ts.Node) => {
node.forEachChild(visitNode);
if (!ts.isClassDeclaration(node)) {
return;
}
const {isDirectiveOrComponent, isAbstractDirective, kind} =
this._analyzeClassDeclaration(node);
if (isDirectiveOrComponent) {
if (isAbstractDirective) {
abstractDirectives.add(node);
} else {
nonAbstractDirectives.add(node);
}
} else if (kind === ClassKind.DIRECTIVE) {
abstractDirectives.add(node);
result.add(node);
const {inferredKind, decoratedType} = this._analyzeClassDeclaration(node);
if (decoratedType !== null) {
declarations.set(node, decoratedType);
return;
}
if (inferredKind === InferredKind.DIRECTIVE) {
detectedAbstractDirectives.add(node);
} else if (inferredKind === InferredKind.AMBIGUOUS) {
ambiguousClasses.add(node);
} else {
if (kind === ClassKind.AMBIGUOUS) {
ambiguous.add(node);
}
undecoratedClasses.add(node);
}
};
sourceFiles.forEach(sourceFile => sourceFile.forEachChild(visitNode));
// We collected all undecorated class declarations which inherit from abstract directives.
// For such abstract directives, the derived classes also need to be migrated.
undecoratedClasses.forEach(node => {
for (const {node: baseClass} of findBaseClassDeclarations(node, this.typeChecker)) {
// If the undecorated class inherits from a non-abstract directive, skip the current
// class. We do this because undecorated classes which inherit metadata from non-abstract
// directives are handled in the `undecorated-classes-with-di` migration that copies
// inherited metadata into an explicit decorator.
if (nonAbstractDirectives.has(baseClass)) {
break;
} else if (abstractDirectives.has(baseClass)) {
result.add(node);
// In case the undecorated class previously could not be detected as directive,
// remove it from the ambiguous set as we now know that it's a guaranteed directive.
ambiguous.delete(node);
/**
* Checks the inheritance of the given set of classes. It removes classes from the
* detected abstract directives set when they inherit from a non-abstract Angular
* declaration. e.g. an abstract directive can never extend from a component.
*
* If a class inherits from an abstract directive though, we will migrate them too
* as derived classes also need to be decorated. This has been done for a simpler mental
* model and reduced complexity in the Angular compiler. See migration plan document.
*/
const checkInheritanceOfClasses = (classes: Set<ts.ClassDeclaration>) => {
classes.forEach(node => {
for (const {node: baseClass} of findBaseClassDeclarations(node, this.typeChecker)) {
if (!declarations.has(baseClass)) {
continue;
}
// If the undecorated class inherits from an abstract directive, always migrate it.
// Derived undecorated classes of abstract directives are always also considered
// abstract directives and need to be decorated too. This is necessary as otherwise
// the inheritance chain cannot be resolved by the Angular compiler. e.g. when it
// flattens directive metadata for type checking. In the other case, we never want
// to migrate a class if it extends from a non-abstract Angular declaration. That
// is an unsupported pattern as of v9 and was previously handled with the
// `undecorated-classes-with-di` migration (which copied the inherited decorator).
if (declarations.get(baseClass) === DeclarationType.ABSTRACT_DIRECTIVE) {
detectedAbstractDirectives.add(node);
} else {
detectedAbstractDirectives.delete(node);
}
ambiguousClasses.delete(node);
break;
}
}
});
});
};
return {result, ambiguous};
// Check inheritance of any detected abstract directive. We want to remove
// classes that are not eligible abstract directives due to inheritance. i.e.
// if a class extends from a component, it cannot be a derived abstract directive.
checkInheritanceOfClasses(detectedAbstractDirectives);
// Update the class declarations to reflect the detected abstract directives. This is
// then used later when we check for undecorated classes that inherit from an abstract
// directive and need to be decorated.
detectedAbstractDirectives.forEach(
n => declarations.set(n, DeclarationType.ABSTRACT_DIRECTIVE));
// Check ambiguous and undecorated classes if they inherit from an abstract directive.
// If they do, we want to migrate them too. See function definition for more details.
checkInheritanceOfClasses(ambiguousClasses);
checkInheritanceOfClasses(undecoratedClasses);
return {detectedAbstractDirectives, ambiguousClasses};
}
/**
@ -186,19 +227,30 @@ export class UndecoratedClassesWithDecoratedFieldsTransform {
*/
private _analyzeClassDeclaration(node: ts.ClassDeclaration): AnalyzedClass {
const ngDecorators = node.decorators && getAngularDecorators(this.typeChecker, node.decorators);
const kind = this._determineClassKind(node);
const inferredKind = this._determineClassKind(node);
if (ngDecorators === undefined || ngDecorators.length === 0) {
return {isDirectiveOrComponent: false, isAbstractDirective: false, kind};
return {decoratedType: null, inferredKind};
}
const directiveDecorator = ngDecorators.find(({name}) => name === 'Directive');
const componentDecorator = ngDecorators.find(({name}) => name === 'Component');
const pipeDecorator = ngDecorators.find(({name}) => name === 'Pipe');
const injectableDecorator = ngDecorators.find(({name}) => name === 'Injectable');
const isAbstractDirective =
directiveDecorator !== undefined && this._isAbstractDirective(directiveDecorator);
return {
isDirectiveOrComponent: !!directiveDecorator || !!componentDecorator,
isAbstractDirective,
kind,
};
let decoratedType: DeclarationType|null = null;
if (isAbstractDirective) {
decoratedType = DeclarationType.ABSTRACT_DIRECTIVE;
} else if (componentDecorator !== undefined) {
decoratedType = DeclarationType.COMPONENT;
} else if (directiveDecorator !== undefined) {
decoratedType = DeclarationType.DIRECTIVE;
} else if (pipeDecorator !== undefined) {
decoratedType = DeclarationType.PIPE;
} else if (injectableDecorator !== undefined) {
decoratedType = DeclarationType.INJECTABLE;
}
return {decoratedType, inferredKind};
}
/**
@ -228,8 +280,8 @@ export class UndecoratedClassesWithDecoratedFieldsTransform {
* e.g. lifecycle hooks or decorated members like `@Input` or `@Output` are
* considered Angular features..
*/
private _determineClassKind(node: ts.ClassDeclaration): ClassKind {
let usage = ClassKind.UNKNOWN;
private _determineClassKind(node: ts.ClassDeclaration): InferredKind {
let usage = InferredKind.UNKNOWN;
for (const member of node.members) {
const propertyName = member.name !== undefined ? getPropertyNameText(member.name) : null;
@ -237,7 +289,7 @@ export class UndecoratedClassesWithDecoratedFieldsTransform {
// If the class declares any of the known directive lifecycle hooks, we can
// immediately exit the loop as the class is guaranteed to be a directive.
if (propertyName !== null && DIRECTIVE_LIFECYCLE_HOOKS.has(propertyName)) {
return ClassKind.DIRECTIVE;
return InferredKind.DIRECTIVE;
}
const ngDecorators = member.decorators !== undefined ?
@ -245,7 +297,7 @@ export class UndecoratedClassesWithDecoratedFieldsTransform {
[];
for (const {name} of ngDecorators) {
if (DIRECTIVE_FIELD_DECORATORS.has(name)) {
return ClassKind.DIRECTIVE;
return InferredKind.DIRECTIVE;
}
}
@ -253,10 +305,27 @@ export class UndecoratedClassesWithDecoratedFieldsTransform {
// the given class is a directive, update the kind and continue looking for other
// members that would unveil a more specific kind (i.e. being a directive).
if (propertyName !== null && AMBIGUOUS_LIFECYCLE_HOOKS.has(propertyName)) {
usage = ClassKind.AMBIGUOUS;
usage = InferredKind.AMBIGUOUS;
}
}
return usage;
}
/**
* Checks whether a given class has been reported as ambiguous in previous
* migration run. e.g. when build targets are migrated first, and then test
* targets that have an overlap with build source files, the same class
* could be detected as ambiguous.
*/
private _hasBeenReportedAsAmbiguous(node: ts.ClassDeclaration): boolean {
const sourceFile = node.getSourceFile();
const leadingComments = ts.getLeadingCommentRanges(sourceFile.text, node.pos);
if (leadingComments === undefined) {
return false;
}
return leadingComments.some(
({kind, pos, end}) => kind === ts.SyntaxKind.SingleLineCommentTrivia &&
sourceFile.text.substring(pos, end).includes(`TODO: ${AMBIGUOUS_CLASS_TODO}`));
}
}

View File

@ -136,24 +136,36 @@ describe('Google3 undecorated classes with decorated fields TSLint rule', () =>
it('should not change decorated classes', () => {
writeFile('/index.ts', `
import { Input, Component, Output, EventEmitter } from '@angular/core';
import { Input, Component, Directive, Pipe, Injectable } from '@angular/core';
@Component({})
export class Base {
export class MyComp {
@Input() isActive: boolean;
}
@Directive({selector: 'dir'})
export class MyDir {
@Input() isActive: boolean;
}
export class Child extends Base {
@Output() clicked = new EventEmitter<void>();
@Injectable()
export class MyService {
ngOnDestroy() {}
}
@Pipe({name: 'my-pipe'})
export class MyPipe {
ngOnDestroy() {}
}
`);
runTSLint(true);
const content = getFile('/index.ts');
expect(content).toContain(
`import { Input, Component, Output, EventEmitter, Directive } from '@angular/core';`);
expect(content).toContain(`@Component({})\n export class Base {`);
expect(content).toContain(`@Directive()\nexport class Child extends Base {`);
expect(content).toMatch(/@Component\({}\)\s+export class MyComp {/);
expect(content).toMatch(/@Directive\({selector: 'dir'}\)\s+export class MyDir {/);
expect(content).toMatch(/@Injectable\(\)\s+export class MyService {/);
expect(content).toMatch(/@Pipe\({name: 'my-pipe'}\)\s+export class MyPipe {/);
expect(content).not.toContain('TODO');
});
it('should add @Directive to undecorated classes that have @Output', () => {

View File

@ -9,6 +9,7 @@
/**
* Template string function that can be used to dedent the resulting
* string literal. The smallest common indentation will be omitted.
* Additionally, whitespace in empty lines is removed.
*/
export function dedent(strings: TemplateStringsArray, ...values: any[]) {
let joinedString = '';
@ -24,5 +25,7 @@ export function dedent(strings: TemplateStringsArray, ...values: any[]) {
const minLineIndent = Math.min(...matches.map(el => el.length));
const omitMinIndentRegex = new RegExp(`^[ \\t]{${minLineIndent}}`, 'gm');
return minLineIndent > 0 ? joinedString.replace(omitMinIndentRegex, '') : joinedString;
const omitEmptyLineWhitespaceRegex = /^[ \t]+$/gm;
const result = minLineIndent > 0 ? joinedString.replace(omitMinIndentRegex, '') : joinedString;
return result.replace(omitEmptyLineWhitespaceRegex, '');
}

View File

@ -11,6 +11,7 @@ import {TempScopedNodeJsSyncHost} from '@angular-devkit/core/node/testing';
import {HostTree} from '@angular-devkit/schematics';
import {SchematicTestRunner, UnitTestTree} from '@angular-devkit/schematics/testing';
import * as shx from 'shelljs';
import {dedent} from './helpers';
describe('Undecorated classes with decorated fields migration', () => {
let runner: SchematicTestRunner;
@ -117,26 +118,253 @@ describe('Undecorated classes with decorated fields migration', () => {
expect(tree.readContent('/index.ts')).toContain(`@Directive()\nexport class Base {`);
});
it('should not change decorated classes', async () => {
writeFile('/index.ts', `
import { Input, Component, Output, EventEmitter } from '@angular/core';
it('should not migrate classes decorated with @Component', async () => {
writeFile('/index.ts', dedent`
import {Input, Component} from '@angular/core';
@Component({})
@Component({selector: 'hello', template: 'hello'})
export class Base {
@Input() isActive: boolean;
}
export class Child extends Base {
@Output() clicked = new EventEmitter<void>();
@Component({selector: 'hello', template: 'hello'})
export class Derived extends Base {
ngOnDestroy() {}
}
`);
await runMigration();
const content = tree.readContent('/index.ts');
expect(content).toContain(
`import { Input, Component, Output, EventEmitter, Directive } from '@angular/core';`);
expect(content).toContain(`@Component({})\n export class Base {`);
expect(content).toContain(`@Directive()\nexport class Child extends Base {`);
expect(warnings.length).toBe(0);
expect(tree.readContent('/index.ts')).toBe(dedent`
import {Input, Component} from '@angular/core';
@Component({selector: 'hello', template: 'hello'})
export class Base {
@Input() isActive: boolean;
}
@Component({selector: 'hello', template: 'hello'})
export class Derived extends Base {
ngOnDestroy() {}
}
`);
});
it('should not migrate classes decorated with @Directive', async () => {
writeFile('/index.ts', dedent`
import {Input, Directive} from '@angular/core';
@Directive()
export class Base {
@Input() isActive: boolean;
}
@Directive({selector: 'other'})
export class Other extends Base {
ngOnDestroy() {}
}
`);
await runMigration();
expect(warnings.length).toBe(0);
expect(tree.readContent('/index.ts')).toBe(dedent`
import {Input, Directive} from '@angular/core';
@Directive()
export class Base {
@Input() isActive: boolean;
}
@Directive({selector: 'other'})
export class Other extends Base {
ngOnDestroy() {}
}
`);
});
it('should not migrate when class inherits from component', async () => {
writeFile('/index.ts', dedent`
import {Input, Component} from '@angular/core';
@Component({selector: 'my-comp', template: 'my-comp'})
export class MyComp {}
export class WithDisabled extends MyComp {
@Input() disabled: boolean;
}
`);
await runMigration();
expect(warnings.length).toBe(0);
expect(tree.readContent('/index.ts')).toBe(dedent`
import {Input, Component} from '@angular/core';
@Component({selector: 'my-comp', template: 'my-comp'})
export class MyComp {}
export class WithDisabled extends MyComp {
@Input() disabled: boolean;
}
`);
});
it('should not migrate when class inherits from pipe', async () => {
writeFile('/index.ts', dedent`
import {Pipe} from '@angular/core';
@Pipe({name: 'my-pipe'})
export class MyPipe {}
export class PipeDerived extends MyPipe {
ngOnDestroy() {}
}
`);
await runMigration();
expect(warnings.length).toBe(0);
expect(tree.readContent('/index.ts')).toBe(dedent`
import {Pipe} from '@angular/core';
@Pipe({name: 'my-pipe'})
export class MyPipe {}
export class PipeDerived extends MyPipe {
ngOnDestroy() {}
}
`);
});
it('should not migrate when class inherits from injectable', async () => {
writeFile('/index.ts', dedent`
import {Injectable} from '@angular/core';
@Injectable()
export class MyService {}
export class ServiceDerived extends MyService {
ngOnDestroy() {}
}
`);
await runMigration();
expect(warnings.length).toBe(0);
expect(tree.readContent('/index.ts')).toBe(dedent`
import {Injectable} from '@angular/core';
@Injectable()
export class MyService {}
export class ServiceDerived extends MyService {
ngOnDestroy() {}
}
`);
});
it('should not migrate when class inherits from directive', async () => {
writeFile('/index.ts', dedent`
import {Directive} from '@angular/core';
@Directive({selector: 'hello'})
export class MyDir {}
export class DirDerived extends MyDir {
ngOnDestroy() {}
}
`);
await runMigration();
expect(warnings.length).toBe(0);
expect(tree.readContent('/index.ts')).toBe(dedent`
import {Directive} from '@angular/core';
@Directive({selector: 'hello'})
export class MyDir {}
export class DirDerived extends MyDir {
ngOnDestroy() {}
}
`);
});
it('should not add multiple TODOs for ambiguous classes', async () => {
writeFile('/angular.json', JSON.stringify({
projects: {
test: {
architect: {
build: {options: {tsConfig: './tsconfig.json'}},
test: {options: {tsConfig: './tsconfig.json'}},
}
}
}
}));
writeFile('/index.ts', dedent`
export class MyService {
ngOnDestroy() {}
}
`);
await runMigration();
expect(tree.readContent('/index.ts')).toBe(dedent`
// TODO: Add Angular decorator.
export class MyService {
ngOnDestroy() {}
}
`);
});
it('should not report pipe using `ngOnDestroy` as ambiguous', async () => {
writeFile('/index.ts', dedent`
import {Pipe} from '@angular/core';
@Pipe({name: 'my-pipe'})
export class MyPipe {
ngOnDestroy() {}
transform() {}
}
`);
await runMigration();
expect(warnings.length).toBe(0);
expect(tree.readContent('/index.ts')).toBe(dedent`
import {Pipe} from '@angular/core';
@Pipe({name: 'my-pipe'})
export class MyPipe {
ngOnDestroy() {}
transform() {}
}
`);
});
it('should not report injectable using `ngOnDestroy` as ambiguous', async () => {
writeFile('/index.ts', dedent`
import {Injectable} from '@angular/core';
@Injectable({providedIn: 'root'})
export class MyService {
ngOnDestroy() {}
}
`);
await runMigration();
expect(warnings.length).toBe(0);
expect(tree.readContent('/index.ts')).toBe(dedent`
import {Injectable} from '@angular/core';
@Injectable({providedIn: 'root'})
export class MyService {
ngOnDestroy() {}
}
`);
});
it('should add @Directive to undecorated classes that have @Output', async () => {
@ -298,6 +526,8 @@ describe('Undecorated classes with decorated fields migration', () => {
await runMigration();
const fileContent = tree.readContent('/index.ts');
expect(warnings.length).toBe(0);
expect(fileContent).toContain(`import { Input, Directive, NgModule } from '@angular/core';`);
expect(fileContent).toMatch(/@Directive\(\)\s+export class Base/);
expect(fileContent).toMatch(/@Directive\(\)\s+export class DerivedA/);
@ -305,6 +535,7 @@ describe('Undecorated classes with decorated fields migration', () => {
expect(fileContent).toMatch(/@Directive\(\)\s+export class DerivedC/);
expect(fileContent).toMatch(/}\s+@Directive\(\{selector: 'my-comp'}\)\s+export class MyComp/);
expect(fileContent).toMatch(/}\s+export class MyCompWrapped/);
expect(fileContent).not.toContain('TODO: Add Angular decorator');
});
it('should add @Directive to derived undecorated classes of abstract directives', async () => {

View File

@ -70,8 +70,8 @@ export interface Injectable {
* - 'root' : The application-level injector in most apps.
* - 'platform' : A special singleton platform injector shared by all
* applications on the page.
* - 'any' : Provides a unique instance in every module (including lazy modules) that injects the
* token.
* - 'any' : Provides a unique instance in each lazy loaded module while all eagerly loaded
* modules share one instance.
*
*/
providedIn?: Type<any>|'root'|'platform'|'any'|null;

View File

@ -155,14 +155,6 @@ export class ComponentFactory<T> extends viewEngine_ComponentFactory<T> {
const rootFlags = this.componentDef.onPush ? LViewFlags.Dirty | LViewFlags.IsRoot :
LViewFlags.CheckAlways | LViewFlags.IsRoot;
// Check whether this Component needs to be isolated from other components, i.e. whether it
// should be placed into its own (empty) root context or existing root context should be used.
// Note: this is internal-only convention and might change in the future, so it should not be
// relied upon externally.
const isIsolated = typeof rootSelectorOrNode === 'string' &&
/^#root-ng-internal-isolated-\d+/.test(rootSelectorOrNode);
const rootContext = createRootContext();
// Create the root view. Uses empty TView and ContentTemplate.
@ -232,12 +224,10 @@ export class ComponentFactory<T> extends viewEngine_ComponentFactory<T> {
this.componentType, component,
createElementRef(viewEngine_ElementRef, tElementNode, rootLView), rootLView, tElementNode);
if (!rootSelectorOrNode || isIsolated) {
// The host element of the internal or isolated root view is attached to the component's host
// view node.
ngDevMode && assertNodeOfPossibleTypes(rootTView.node, TNodeType.View);
rootTView.node!.child = tElementNode;
}
// The host element of the internal root view is attached to the component's host view node.
ngDevMode && assertNodeOfPossibleTypes(rootTView.node, [TNodeType.View]);
rootTView.node!.child = tElementNode;
return componentRef;
}
}

View File

@ -99,8 +99,12 @@ let nextNgElementId = 0;
export function bloomAdd(
injectorIndex: number, tView: TView, type: Type<any>|InjectionToken<any>|string): void {
ngDevMode && assertEqual(tView.firstCreatePass, true, 'expected firstCreatePass to be true');
let id: number|undefined =
typeof type !== 'string' ? (type as any)[NG_ELEMENT_ID] : type.charCodeAt(0) || 0;
let id: number|undefined;
if (typeof type === 'string') {
id = type.charCodeAt(0) || 0;
} else if (type.hasOwnProperty(NG_ELEMENT_ID)) {
id = (type as any)[NG_ELEMENT_ID];
}
// Set a unique ID on the directive type, so if something tries to inject the directive,
// we can easily retrieve the ID and hash it into the bloom bit that should be checked.
@ -267,7 +271,7 @@ export function diPublicInInjector(
export function injectAttributeImpl(tNode: TNode, attrNameToInject: string): string|null {
ngDevMode &&
assertNodeOfPossibleTypes(
tNode, TNodeType.Container, TNodeType.Element, TNodeType.ElementContainer);
tNode, [TNodeType.Container, TNodeType.Element, TNodeType.ElementContainer]);
ngDevMode && assertDefined(tNode, 'expecting tNode');
if (attrNameToInject === 'class') {
return tNode.classes;
@ -584,7 +588,9 @@ export function bloomHashBitOrFactory(token: Type<any>|InjectionToken<any>|strin
if (typeof token === 'string') {
return token.charCodeAt(0) || 0;
}
const tokenId: number|undefined = (token as any)[NG_ELEMENT_ID];
const tokenId: number|undefined =
// First check with `hasOwnProperty` so we don't get an inherited ID.
token.hasOwnProperty(NG_ELEMENT_ID) ? (token as any)[NG_ELEMENT_ID] : undefined;
// Negative token IDs are used for special objects such as `Injector`
return (typeof tokenId === 'number' && tokenId > 0) ? tokenId & BLOOM_MASK : tokenId;
}

View File

@ -9,7 +9,7 @@ import '../util/ng_i18n_closure_mode';
import {DEFAULT_LOCALE_ID, getPluralCase} from '../i18n/localization';
import {getTemplateContent, SRCSET_ATTRS, URI_ATTRS, VALID_ATTRS, VALID_ELEMENTS} from '../sanitization/html_sanitizer';
import {InertBodyHelper} from '../sanitization/inert_body';
import {getInertBodyHelper} from '../sanitization/inert_body';
import {_sanitizeUrl, sanitizeSrcset} from '../sanitization/url_sanitizer';
import {addAllToArray} from '../util/array_utils';
import {assertDataInRange, assertDefined, assertEqual} from '../util/assert';
@ -1233,7 +1233,7 @@ function icuStart(
function parseIcuCase(
unsafeHtml: string, parentIndex: number, nestedIcus: IcuExpression[], tIcus: TIcu[],
expandoStartIndex: number): IcuCase {
const inertBodyHelper = new InertBodyHelper(getDocument());
const inertBodyHelper = getInertBodyHelper(getDocument());
const inertBodyElement = inertBodyHelper.getInertBodyElement(unsafeHtml);
if (!inertBodyElement) {
throw new Error('Unable to generate inert body element');

View File

@ -9,8 +9,7 @@ import {InjectFlags, InjectionToken, resolveForwardRef} from '../../di';
import {ɵɵinject} from '../../di/injector_compatibility';
import {Type} from '../../interface/type';
import {getOrCreateInjectable, injectAttributeImpl} from '../di';
import {TDirectiveHostNode, TNodeType} from '../interfaces/node';
import {assertNodeOfPossibleTypes} from '../node_assert';
import {TDirectiveHostNode} from '../interfaces/node';
import {getLView, getPreviousOrParentTNode} from '../state';
/**

View File

@ -128,7 +128,7 @@ function listenerInternal(
ngDevMode &&
assertNodeOfPossibleTypes(
tNode, TNodeType.Element, TNodeType.Container, TNodeType.ElementContainer);
tNode, [TNodeType.Element, TNodeType.Container, TNodeType.ElementContainer]);
let processOutputs = true;

View File

@ -15,6 +15,7 @@ import {assertDataInRange, assertDefined, assertDomNode, assertEqual, assertGrea
import {createNamedArrayType} from '../../util/named_array_type';
import {initNgDevMode} from '../../util/ng_dev_mode';
import {normalizeDebugBindingName, normalizeDebugBindingValue} from '../../util/ng_reflect';
import {stringify} from '../../util/stringify';
import {assertFirstCreatePass, assertLContainer, assertLView} from '../assert';
import {attachPatchData} from '../context_discovery';
import {getFactoryDef} from '../definition';
@ -272,7 +273,7 @@ export function assignTViewNodeToLView(
let tNode = tView.node;
if (tNode == null) {
ngDevMode && tParentNode &&
assertNodeOfPossibleTypes(tParentNode, TNodeType.Element, TNodeType.Container);
assertNodeOfPossibleTypes(tParentNode, [TNodeType.Element, TNodeType.Container]);
tView.node = tNode = createTNode(
tView,
tParentNode as TElementNode | TContainerNode | null, //
@ -794,22 +795,6 @@ export function storeCleanupWithContext(
}
}
/**
* Saves the cleanup function itself in LView.cleanupInstances.
*
* This is necessary for functions that are wrapped with their contexts, like in renderer2
* listeners.
*
* On the first template pass, the index of the cleanup function is saved in TView.
*/
export function storeCleanupFn(tView: TView, lView: LView, cleanupFn: Function): void {
getLCleanup(lView).push(cleanupFn);
if (tView.firstCreatePass) {
getTViewCleanup(tView).push(lView[CLEANUP]!.length - 1, null);
}
}
/**
* Constructs a TNode object from the arguments.
*
@ -1278,7 +1263,7 @@ function instantiateAllDirectives(
const isComponent = isComponentDef(def);
if (isComponent) {
ngDevMode && assertNodeOfPossibleTypes(tNode, TNodeType.Element);
ngDevMode && assertNodeOfPossibleTypes(tNode, [TNodeType.Element]);
addComponentLogic(lView, tNode as TElementNode, def as ComponentDef<any>);
}
@ -1366,7 +1351,7 @@ function findDirectiveDefMatches(
ngDevMode && assertFirstCreatePass(tView);
ngDevMode &&
assertNodeOfPossibleTypes(
tNode, TNodeType.Element, TNodeType.ElementContainer, TNodeType.Container);
tNode, [TNodeType.Element, TNodeType.ElementContainer, TNodeType.Container]);
const registry = tView.directiveRegistry;
let matches: any[]|null = null;
if (registry) {
@ -1377,6 +1362,12 @@ function findDirectiveDefMatches(
diPublicInInjector(getOrCreateNodeInjectorForNode(tNode, viewData), tView, def.type);
if (isComponentDef(def)) {
ngDevMode &&
assertNodeOfPossibleTypes(
tNode, [TNodeType.Element],
`"${tNode.tagName}" tags cannot be used as component hosts. ` +
`Please use a different tag to activate the ${
stringify(def.type)} component.`);
if (tNode.flags & TNodeFlags.isComponentHost) throwMultipleComponentError(tNode);
markAsComponentHost(tView, tNode);
// The component is always stored first with directives after.

View File

@ -26,12 +26,14 @@ export function assertNodeType(tNode: TNode, type: TNodeType): asserts tNode is
assertEqual(tNode.type, type, `should be a ${typeName(type)}`);
}
export function assertNodeOfPossibleTypes(tNode: TNode|null, ...types: TNodeType[]): void {
export function assertNodeOfPossibleTypes(
tNode: TNode|null, types: TNodeType[], message?: string): void {
assertDefined(tNode, 'should be called with a TNode');
const found = types.some(type => tNode.type === type);
assertEqual(
found, true,
`Should be one of ${types.map(typeName).join(', ')} but got ${typeName(tNode.type)}`);
message ??
`Should be one of ${types.map(typeName).join(', ')} but got ${typeName(tNode.type)}`);
}
export function assertNodeNotOfTypes(tNode: TNode, types: TNodeType[], message?: string): void {

View File

@ -552,7 +552,7 @@ function getRenderParent(tView: TView, tNode: TNode, currentView: LView): REleme
} else {
// We are inserting a root element of the component view into the component host element and
// it should always be eager.
ngDevMode && assertNodeOfPossibleTypes(hostTNode, TNodeType.Element);
ngDevMode && assertNodeOfPossibleTypes(hostTNode, [TNodeType.Element]);
return currentView[HOST];
}
} else {
@ -698,10 +698,10 @@ export function appendChild(
*/
function getFirstNativeNode(lView: LView, tNode: TNode|null): RNode|null {
if (tNode !== null) {
ngDevMode &&
assertNodeOfPossibleTypes(
tNode, TNodeType.Element, TNodeType.Container, TNodeType.ElementContainer,
TNodeType.IcuContainer, TNodeType.Projection);
ngDevMode && assertNodeOfPossibleTypes(tNode, [
TNodeType.Element, TNodeType.Container, TNodeType.ElementContainer, TNodeType.IcuContainer,
TNodeType.Projection
]);
const tNodeType = tNode.type;
if (tNodeType === TNodeType.Element) {
@ -778,10 +778,10 @@ function applyNodes(
renderParent: RElement|null, beforeNode: RNode|null, isProjection: boolean) {
while (tNode != null) {
ngDevMode && assertTNodeForLView(tNode, lView);
ngDevMode &&
assertNodeOfPossibleTypes(
tNode, TNodeType.Container, TNodeType.Element, TNodeType.ElementContainer,
TNodeType.Projection, TNodeType.Projection, TNodeType.IcuContainer);
ngDevMode && assertNodeOfPossibleTypes(tNode, [
TNodeType.Container, TNodeType.Element, TNodeType.ElementContainer, TNodeType.Projection,
TNodeType.IcuContainer
]);
const rawSlotValue = lView[tNode.index];
const tNodeType = tNode.type;
if (isProjection) {
@ -798,7 +798,7 @@ function applyNodes(
applyProjectionRecursive(
renderer, action, lView, tNode as TProjectionNode, renderParent, beforeNode);
} else {
ngDevMode && assertNodeOfPossibleTypes(tNode, TNodeType.Element, TNodeType.Container);
ngDevMode && assertNodeOfPossibleTypes(tNode, [TNodeType.Element, TNodeType.Container]);
applyToElementOrContainer(action, renderer, renderParent, rawSlotValue, beforeNode);
}
}

View File

@ -325,7 +325,7 @@ function createSpecialToken(lView: LView, tNode: TNode, read: any): any {
} else if (read === ViewContainerRef) {
ngDevMode &&
assertNodeOfPossibleTypes(
tNode, TNodeType.Element, TNodeType.Container, TNodeType.ElementContainer);
tNode, [TNodeType.Element, TNodeType.Container, TNodeType.ElementContainer]);
return createContainerRef(
ViewContainerRef, ViewEngine_ElementRef,
tNode as TElementNode | TContainerNode | TElementContainerNode, lView);

View File

@ -340,7 +340,7 @@ export function createContainerRef(
ngDevMode &&
assertNodeOfPossibleTypes(
hostTNode, TNodeType.Container, TNodeType.Element, TNodeType.ElementContainer);
hostTNode, [TNodeType.Container, TNodeType.Element, TNodeType.ElementContainer]);
let lContainer: LContainer;
const slotValue = hostView[hostTNode.index];

View File

@ -11,7 +11,7 @@ import {ChangeDetectorRef as viewEngine_ChangeDetectorRef} from '../change_detec
import {ViewContainerRef as viewEngine_ViewContainerRef} from '../linker/view_container_ref';
import {EmbeddedViewRef as viewEngine_EmbeddedViewRef, InternalViewRef as viewEngine_InternalViewRef} from '../linker/view_ref';
import {assertDefined} from '../util/assert';
import {checkNoChangesInRootView, checkNoChangesInternal, detectChangesInRootView, detectChangesInternal, markViewDirty, storeCleanupFn} from './instructions/shared';
import {checkNoChangesInRootView, checkNoChangesInternal, detectChangesInRootView, detectChangesInternal, markViewDirty, storeCleanupWithContext} from './instructions/shared';
import {CONTAINER_HEADER_OFFSET} from './interfaces/container';
import {TElementNode, TNode, TNodeType, TViewNode} from './interfaces/node';
import {isLContainer} from './interfaces/type_checks';
@ -88,7 +88,7 @@ export class ViewRef<T> implements viewEngine_EmbeddedViewRef<T>, viewEngine_Int
}
onDestroy(callback: Function) {
storeCleanupFn(this._lView[TVIEW], this._lView, callback);
storeCleanupWithContext(this._lView[TVIEW], this._lView, null, callback);
}
/**
@ -324,10 +324,10 @@ function collectNativeNodes(
tView: TView, lView: LView, tNode: TNode|null, result: any[],
isProjection: boolean = false): any[] {
while (tNode !== null) {
ngDevMode &&
assertNodeOfPossibleTypes(
tNode, TNodeType.Element, TNodeType.Container, TNodeType.Projection,
TNodeType.ElementContainer, TNodeType.IcuContainer);
ngDevMode && assertNodeOfPossibleTypes(tNode, [
TNodeType.Element, TNodeType.Container, TNodeType.Projection, TNodeType.ElementContainer,
TNodeType.IcuContainer
]);
const lNode = lView[tNode.index];
if (lNode !== null) {

View File

@ -7,7 +7,7 @@
*/
import {isDevMode} from '../util/is_dev_mode';
import {InertBodyHelper} from './inert_body';
import {getInertBodyHelper, InertBodyHelper} from './inert_body';
import {_sanitizeUrl, sanitizeSrcset} from './url_sanitizer';
function tagSet(tags: string): {[k: string]: boolean} {
@ -245,7 +245,7 @@ let inertBodyHelper: InertBodyHelper;
export function _sanitizeHtml(defaultDoc: any, unsafeHtmlInput: string): string {
let inertBodyElement: HTMLElement|null = null;
try {
inertBodyHelper = inertBodyHelper || new InertBodyHelper(defaultDoc);
inertBodyHelper = inertBodyHelper || getInertBodyHelper(defaultDoc);
// Make sure unsafeHtml is actually a string (TypeScript types are not enforced at runtime).
let unsafeHtml = unsafeHtmlInput ? String(unsafeHtmlInput) : '';
inertBodyElement = inertBodyHelper.getInertBodyElement(unsafeHtml);

View File

@ -7,89 +7,29 @@
*/
/**
* This helper class is used to get hold of an inert tree of DOM elements containing dirty HTML
* This helper is used to get hold of an inert tree of DOM elements containing dirty HTML
* that needs sanitizing.
* Depending upon browser support we must use one of three strategies for doing this.
* Support: Safari 10.x -> XHR strategy
* Support: Firefox -> DomParser strategy
* Default: InertDocument strategy
* Depending upon browser support we use one of two strategies for doing this.
* Default: DOMParser strategy
* Fallback: InertDocument strategy
*/
export class InertBodyHelper {
private inertDocument: Document;
constructor(private defaultDoc: Document) {
this.inertDocument = this.defaultDoc.implementation.createHTMLDocument('sanitization-inert');
let inertBodyElement = this.inertDocument.body;
if (inertBodyElement == null) {
// usually there should be only one body element in the document, but IE doesn't have any, so
// we need to create one.
const inertHtml = this.inertDocument.createElement('html');
this.inertDocument.appendChild(inertHtml);
inertBodyElement = this.inertDocument.createElement('body');
inertHtml.appendChild(inertBodyElement);
}
inertBodyElement.innerHTML = '<svg><g onload="this.parentNode.remove()"></g></svg>';
if (inertBodyElement.querySelector && !inertBodyElement.querySelector('svg')) {
// We just hit the Safari 10.1 bug - which allows JS to run inside the SVG G element
// so use the XHR strategy.
this.getInertBodyElement = this.getInertBodyElement_XHR;
return;
}
inertBodyElement.innerHTML = '<svg><p><style><img src="</style><img src=x onerror=alert(1)//">';
if (inertBodyElement.querySelector && inertBodyElement.querySelector('svg img')) {
// We just hit the Firefox bug - which prevents the inner img JS from being sanitized
// so use the DOMParser strategy, if it is available.
// If the DOMParser is not available then we are not in Firefox (Server/WebWorker?) so we
// fall through to the default strategy below.
if (isDOMParserAvailable()) {
this.getInertBodyElement = this.getInertBodyElement_DOMParser;
return;
}
}
// None of the bugs were hit so it is safe for us to use the default InertDocument strategy
this.getInertBodyElement = this.getInertBodyElement_InertDocument;
}
export function getInertBodyHelper(defaultDoc: Document): InertBodyHelper {
return isDOMParserAvailable() ? new DOMParserHelper() : new InertDocumentHelper(defaultDoc);
}
export interface InertBodyHelper {
/**
* Get an inert DOM element containing DOM created from the dirty HTML string provided.
* The implementation of this is determined in the constructor, when the class is instantiated.
*/
getInertBodyElement: (html: string) => HTMLElement | null;
}
/**
* Use XHR to create and fill an inert body element (on Safari 10.1)
* See
* https://github.com/cure53/DOMPurify/blob/a992d3a75031cb8bb032e5ea8399ba972bdf9a65/src/purify.js#L439-L449
*/
private getInertBodyElement_XHR(html: string) {
// We add these extra elements to ensure that the rest of the content is parsed as expected
// e.g. leading whitespace is maintained and tags like `<meta>` do not get hoisted to the
// `<head>` tag.
html = '<body><remove></remove>' + html + '</body>';
try {
html = encodeURI(html);
} catch {
return null;
}
const xhr = new XMLHttpRequest();
xhr.responseType = 'document';
xhr.open('GET', 'data:text/html;charset=utf-8,' + html, false);
xhr.send(undefined);
const body: HTMLBodyElement = xhr.response.body;
body.removeChild(body.firstChild!);
return body;
}
/**
* Use DOMParser to create and fill an inert body element (on Firefox)
* See https://github.com/cure53/DOMPurify/releases/tag/0.6.7
*
*/
private getInertBodyElement_DOMParser(html: string) {
/**
* Uses DOMParser to create and fill an inert body element.
* This is the default strategy used in browsers that support it.
*/
class DOMParserHelper implements InertBodyHelper {
getInertBodyElement(html: string): HTMLElement|null {
// We add these extra elements to ensure that the rest of the content is parsed as expected
// e.g. leading whitespace is maintained and tags like `<meta>` do not get hoisted to the
// `<head>` tag.
@ -103,14 +43,30 @@ export class InertBodyHelper {
return null;
}
}
}
/**
* Use an HTML5 `template` element, if supported, or an inert body element created via
* `createHtmlDocument` to create and fill an inert DOM element.
* This is the default sane strategy to use if the browser does not require one of the specialised
* strategies above.
*/
private getInertBodyElement_InertDocument(html: string) {
/**
* Use an HTML5 `template` element, if supported, or an inert body element created via
* `createHtmlDocument` to create and fill an inert DOM element.
* This is the fallback strategy if the browser does not support DOMParser.
*/
class InertDocumentHelper implements InertBodyHelper {
private inertDocument: Document;
constructor(private defaultDoc: Document) {
this.inertDocument = this.defaultDoc.implementation.createHTMLDocument('sanitization-inert');
if (this.inertDocument.body == null) {
// usually there should be only one body element in the document, but IE doesn't have any, so
// we need to create one.
const inertHtml = this.inertDocument.createElement('html');
this.inertDocument.appendChild(inertHtml);
const inertBodyElement = this.inertDocument.createElement('body');
inertHtml.appendChild(inertBodyElement);
}
}
getInertBodyElement(html: string): HTMLElement|null {
// Prefer using <template> element if supported.
const templateEl = this.inertDocument.createElement('template');
if ('content' in templateEl) {
@ -164,15 +120,15 @@ export class InertBodyHelper {
}
/**
* We need to determine whether the DOMParser exists in the global context.
* The try-catch is because, on some browsers, trying to access this property
* on window can actually throw an error.
* We need to determine whether the DOMParser exists in the global context and
* supports parsing HTML; HTML parsing support is not as wide as other formats, see
* https://developer.mozilla.org/en-US/docs/Web/API/DOMParser#Browser_compatibility.
*
* @suppress {uselessCode}
*/
function isDOMParserAvailable() {
export function isDOMParserAvailable() {
try {
return !!(window as any).DOMParser;
return !!new (window as any).DOMParser().parseFromString('', 'text/html');
} catch {
return false;
}

Some files were not shown because too many files have changed in this diff Show More