Compare commits

...

81 Commits

Author SHA1 Message Date
5542517b9c docs: add changelog for 5.0.0-rc.7 2017-10-26 19:03:14 -07:00
fef3539608 release: cut the 5.0.0-rc.7 release 2017-10-26 19:01:34 -07:00
f4d5729cb3 fix(compiler): make watch mode work on windows (#19953)
Fixes #19951
PR Close #19953
2017-10-26 21:52:35 -04:00
d343bf7885 fix(compiler): recover from structural errors in watch mode (#19953)
This also changes the compiler so that we throw less often
on structural changes and produce a meaningful state
in the `ng.Program` in case of errors.

Related to #19951

PR Close #19953
2017-10-26 21:52:25 -04:00
9ce7f0e538 fix(compiler): translate emit diagnostics with noEmitOnError: true. (#19953)
This prevents errors reported against `.ngfactory.ts` files show up
as the result of running `ngc`.

Closes #19935
PR Close #19953
2017-10-26 21:52:16 -04:00
4a23df3909 fix(compiler): don’t store invalid state when using listLazyRoutes (#19953)
Previously, `listLazyRoute` would store invalid information in a compiler
internal cache, which lead to incorrect paths that were used during emit.
This commit fixes this.

PR Close #19953
2017-10-26 21:51:43 -04:00
14016c781f fix(service-worker): fix improper call of Observable.merge (#19962)
Observable.merge was called using .call() as if it were an operator
and not an Observable factory. This removes the .call() and uses
the factory properly.

PR Close #19962
2017-10-26 18:16:20 -04:00
47caebfe86 fix(service-worker): don't block initialization on registration (#19936)
Importing ServiceWorkerModule.register() will schedule registration of
the Service Worker inside an APP_INITIALIZER. Previously, the Promise
returned by navigator.serviceWorker.register() was returned from the
initializer function. This has the unwanted side effect of blocking
initialization until the SW is registered. Even worse, if the SW script
fails to load, this can cause the app initialization to fail.

The solution is to not return the registration promise from the
initializer function, essentially decoupling registration from the rest
of the initialization flow.

This change is not unit testable as there are no mocks/adapters yet for
navigator.serviceWorker. A future integration test should cover this case
with better fidelity.

PR Close #19936
2017-10-26 16:10:17 -04:00
5cfd9c6020 fix(service-worker): listen for messages on the right event source (#19954)
Currently, the SwUpdate service doesn't receive messages from the SW.
This is because it attempts to subscribe to the 'message' event on
ServiceWorkerRegistration, when really messages are emitted by the
ServiceWorkerContainer.

This change moves to listening on ServiceWorkerContainer and changes
the mocks to reflect the way the browser actually works.

PR Close #19954
2017-10-26 16:10:07 -04:00
47bc6f105d docs: add changelog for 5.0.0-rc.6 2017-10-25 14:34:42 -07:00
40fa2593a9 release: cut the 5.0.0-rc.6 release 2017-10-25 14:32:11 -07:00
680bcf7b8a build: update to rxjs@5.5.2 (#19931)
PR Close #19931
2017-10-25 15:32:01 -04:00
ef08330341 fix(compiler): automatically set emitDecoratorMetadata when "annotationsAs": "static fields” (#19927)
This is a workaround for https://github.com/angular/tsickle/issues/635.

Fixes #19916
PR Close #19927
2017-10-25 14:26:28 -04:00
6cc042e2ba fix(compiler-cli): produce correct paths for windows output (#19915)
The path mapping was broken for Windows by fc0b1d5b61.
Fixed the path mapping and put code in place to make such a problem
to sneek by again.

PR Close #19915
2017-10-24 19:21:18 -04:00
9b26455740 fix(compiler-cli): only use error collector when needed. (#19912)
The error collector changes behavior of the metadata resolver
in ways that haven't been fully hardened. This changes limits
its use to the lazy route detection and the language service.

Issue: #19906

PR Close #19912
2017-10-24 19:21:13 -04:00
18bce5987c fix(compiler): don’t type check templates with skipTemplateCodegen (#19909)
This change is needed to prevent users’ builds from breaking.

If a user sets `fullTemlateTypeCheck` to true, we will
continue to check the templates even when `skipTemplateCodegen` is true
as well.

Related to #19906

PR Close #19909
2017-10-24 19:21:03 -04:00
f1108fea76 docs: add changelog for 5.0.0-rc.5 2017-10-23 23:28:28 -07:00
64b3e3e41a release: cut the 5.0.0-rc.5 release 2017-10-23 23:27:15 -07:00
a82f863e24 fix(compiler-cli): report all diagnostic error messages (#19886)
This fixes a problem introduced in 8d45fefc31
which modified how diagnostic error messages are reported for structural
metadata errors causing some of the diagnostics to be lost.

PR Close #19886
2017-10-24 00:57:41 -04:00
bde57016c6 docs: add changelog for 5.0.0-rc.4 2017-10-23 17:35:17 -07:00
b16f4bce98 release: cut the 5.0.0-rc.4 release 2017-10-23 17:31:42 -07:00
6bed189e37 build(service-worker): set skipTemplateCodegen for build (#19875)
PR Close #19875
2017-10-23 19:31:42 -04:00
4abacb58f1 fix(aio): add service worker entrypoint to aio build (#19875)
Fixes #19838

PR Close #19875
2017-10-23 19:31:42 -04:00
04200150d5 docs: add service-worker to list of allowed scopes (#19875)
PR Close #19875
2017-10-23 19:31:42 -04:00
fc0b1d5b61 fix(compiler): correctly calculate the outDir if it repeats a parts of the rootDir. (#19836)
Fixes #19718

PR Close #19836
2017-10-23 18:46:04 -04:00
8d45fefc31 refactor(compiler): remove old ngtools api and add listLazyRoutes to new api (#19836)
Usages of `NgTools_InternalApi_NG_2` from `@angular/compiler-cli` will now
throw an error.

Adds `listLazyRoutes` to `@angular/compiler-cli/ngtools2.ts` for getting
the lazy routes of a `ng.Program`.
PR Close #19836
2017-10-23 18:46:04 -04:00
5da96c75a2 fix(bazel): don't console.error from the compile helper (#19879)
This lets other callers of compile() choose different formatting for the diagnostics

PR Close #19879
2017-10-23 16:56:57 -04:00
90d1423fb4 fix(service-worker): include versionedFiles in the manifest hashTable (#19837)
There is no difference in runtime (yet) between versioned and unversioned
files. Theoretically, the SW does not have to cache-bust versioned files,
but the SW doesn't cache bust files on the first request anyway, so in the
common case it doesn't matter. If the hash doesn't match, the SW will cache
bust the file to be sure, which is technically unnecessary, but since the
file itself is versioned, the likelihood of this happening is rare.

This fixes a critical bug where versioned files were erroneously not included
in the hashTable in the generated manifest. This could lead to applications
not updating if only versioned files changed in between versions.

PR Close #19837
2017-10-23 15:11:38 -04:00
910735d732 build: fix yarn install command (--freeze-lockfile --> --frozen-lockfile) 2017-10-20 10:14:30 -07:00
fc86352adf build(aio): use http push for navigation.json 2017-10-20 09:41:02 -07:00
441e01c568 fix(aio): simplify GaService to avoid e2e test failures
The GaService and the E2E specs were unnecessarily complicated and had
arbitrary async timeouts to ensure that the interplay between the GA
library code and the rest of the app worked correctly. This resulted
in potential flaky tests if the timeouts were not adequate; this was
experienced when Travis upgraded to Chrome 62.

The new approach is to block loading of the Analytics library altogether
if there is a `__e2e__` flag set in the `SessionStorage` of the browser.
It doesn't appear to be enough just to set the flag directly on the
window. I think this is because the window gets cleaned when navigation
occurs (but I am not certain).

The downside of this is that we had to add a small piece of extra logic
to the GA snippet in index.html; and we also had to switch from using
`<script async ...>` to a programmatic approach to loading the GA library
which prevents the browser from eagerly fetching the library. This may
be mitigated by adding it to the HTTP/2 push configuration of the Firebase
hosting.

Re-enables the test that was disabled in https://github.com/angular/angular/pull/19784

Closes #19785
2017-10-20 09:40:52 -07:00
14380ff086 build: add warning about changing ngtools_api2 2017-10-20 09:39:30 -07:00
820bb7bd8c revert: ci: use chrome stable (#18307)
This reverts commit 8bcb268140.
2017-10-20 09:38:59 -07:00
230b98d4dd docs: add changelog for 5.0.0-rc.3 2017-10-18 17:23:09 -07:00
065ea926c0 release: cut the 5.0.0-rc.3 release 2017-10-18 17:23:09 -07:00
9b9820858e docs: add changelog for 4.4.6 2017-10-18 17:23:09 -07:00
b922743f6e build: narrow TS version to >=2.4.2 and <2.5 (#19787)
As this is the only version range that we tested against in G3.
We will support newer versions of TypeScript soon 
after the Angular 5 release.

Closes #19750

PR Close #19787
2017-10-18 11:20:23 -07:00
04ab9f1917 fix(common): attempt to JSON.parse errors for JSON responses (#19773)
PR Close #19773
2017-10-18 11:18:58 -07:00
25cbc98979 fix(compiler-cli): do not add references to files outside of rootDir (#19770)
References to resources (such as .css files) that are generated into
the `outDir` directory outside of `rootDir` would cause a spurious
compiler error about not being able to find a files that ends in
'.ngstyle.ts'.

Also fixed a minor issue in compiler error reporting

Fixes: #19765, #19767

PR Close #19770
2017-10-18 11:18:50 -07:00
3861ba2929 docs(changelog): fix typo (#19766)
PR Close #19766
2017-10-18 11:18:42 -07:00
3bcf0cf472 fix(service-worker): PushEvent.data has to be decoded (#19764)
PushEvent.data is not the data object itself, but an instance representing
the data in wire format, with methods to synchronously decode it to JSON,
ArrayBuffer, etc. NGSW assumes all push data is in JSON format.

PR Close #19764
2017-10-18 11:18:34 -07:00
396c2417d9 fix(service-worker): freshness strategy should clone response for cache (#19764)
When Cache.put() is called with a Response, it consumes the response. If
the Response is used for any other purpose (such as satisfying the
original FetchEvent) it must be cloned first.

A bug exists in the mocks used for SW tests, where this condition is not
validated. The bodies of MockResponses can be utilized repeatedly without
erroring in the same way that a real browser would. This bug is fixed by
this commit, which causes tests for the freshness strategy of data caching
to start failing.

The cause of this failure is a second bug in the data caching code, where
the Response is not cloned prior to being passed to Cache.put(). This is
also fixed.

PR Close #19764
2017-10-18 11:18:34 -07:00
fcfb1544e8 docs(aio): changed confusing term (#19762)
Controller should be decorator I believe

PR Close #19762
2017-10-18 11:18:26 -07:00
56774dfb79 fix(compiler-cli): diagnostics file paths relative to cwd, not tsconfig (#19748)
PR Close #19748
2017-10-18 11:18:17 -07:00
c0cc6eeca1 test(animations): ensure :enter callbacks fire on container insertion (#19674)
PR Close #19674
2017-10-18 11:18:08 -07:00
6f2939da62 fix(router): RouterLinkActive should update its state right after checking the children (#19449)
Closes #18983

PR Close #19449
2017-10-18 11:17:55 -07:00
7d1abd9adb build: update to rxjs@5.5.0 (#19345)
PR Close #19345
2017-10-18 11:17:43 -07:00
81173b0d29 fix(aio): make tests less flaky (#19784)
PR Close #19784
2017-10-18 10:19:24 -07:00
b0c7ea8181 Revert "fix(router): RouterLinkActive should update its state right after checking the children (#19449)"
This reverts commit c569b75249.
As it was synched together with 5a9ed2de27
which broke an internal test.
2017-10-18 09:58:41 -07:00
30ecb6e88a Revert "test(animations): ensure :enter callbacks fire on container insertion (#19674)"
This reverts commit 41f57affb6.
As it was synched together with 5a9ed2de27
which broke an internal test.
2017-10-18 09:57:56 -07:00
8d735da5d8 Revert "fix(animations): always fire inner trigger callbacks even if blocked by parent animations (#19753)"
This reverts commit 5a9ed2de27.
As it broke an internal test.
2017-10-18 09:56:59 -07:00
5a9ed2de27 fix(animations): always fire inner trigger callbacks even if blocked by parent animations (#19753)
Closes #19100

PR Close #19753
2017-10-17 20:57:57 -07:00
41f57affb6 test(animations): ensure :enter callbacks fire on container insertion (#19674)
PR Close #19674
2017-10-17 20:57:48 -07:00
c569b75249 fix(router): RouterLinkActive should update its state right after checking the children (#19449)
Closes #18983

PR Close #19449
2017-10-17 20:57:30 -07:00
01e4aa5427 build: remove required BrowserStack run as it fails with “Access denied” (#19769)
See #19768
PR Close #19769
2017-10-17 15:51:40 -07:00
ad130d62d8 fix(compiler): add first bazel test for ng_module (#19703)
We were missing quite a bit of test coverage,
this is the start of recreating it.
PR Close #19703
2017-10-17 15:18:31 -07:00
621f87b2bd fix(service-worker): use posix path resolution for generation of ngsw.json (#19527)
PR Close #19527
2017-10-17 15:18:17 -07:00
64b36190de build: don’t make BrowserStack required as it fails with “Access denied”
See #19768
2017-10-17 14:56:10 -07:00
507290d30d build: fix broken path for animations in .pullapprove (#19453)
PR Close #19453
2017-10-17 10:45:10 -07:00
15a8429b96 fix(service-worker): add missing annotation for SwPush (#19721)
PR Close #19721
2017-10-17 10:38:45 -07:00
9723a362b6 docs(aio): update 2018 events (#19706)
update ac 2017 dates

PR Close #19706
2017-10-17 10:38:37 -07:00
d75a9fabdc ci: validate commit messages correctly when not on master (#19685)
PR Close #19685
2017-10-17 10:38:29 -07:00
9b264c5c78 docs(aio): change in-mem-web-api version for examples (#19668)
PR Close #19668
2017-10-17 10:38:15 -07:00
60bdcd6f5f fix(compiler): generate correct imports for type check blocks (#19582)
Fixes: #19485

PR Close #19582
2017-10-17 10:37:55 -07:00
d035175cdb fix(animations): ensure inner :leave animations do not remove node when skipped (#19532) (#19693)
PR Close #19693
2017-10-16 11:13:58 -07:00
f42d317d2f fix(animations): ensure animateChild() works with all inner leave animations (#19006) (#19532) (#19693)
PR Close #19693
2017-10-16 11:13:58 -07:00
405ccc7195 fix(bazel): fix the output directory for extractor to be genfiles/ instead of bin/ (#19716)
PR Close #19716
2017-10-13 17:31:10 -07:00
836c889baa fix(compiler): prepare for future Bazel semantics of += (#19717)
This is a local mod that was already applied in G3.

PR Close #19717
2017-10-13 16:29:46 -07:00
43f9d917d9 build(aio): fix overwriting with local Angular packages that depend on other local ones (#19655)
PR Close #19655
2017-10-13 09:27:51 -07:00
62c7b7842b test(aio): fix testing of NgPackagesInstaller (#19655)
PR Close #19655
2017-10-13 09:27:51 -07:00
97969a85cd ci(aio): raise payload limit to accommodate the new search feature (#19682)
PR Close #19682
2017-10-13 09:18:42 -07:00
91fcfcb042 feat(aio): add search to 404 page (#19682)
The 404 page will now run a search based on the given URL to offer
suggestions for the page that the user really wanted.

PR Close #19682
2017-10-13 09:18:42 -07:00
88c46feb20 refactor(aio): move SearchResultsComponent into shared module (#19682)
This will allow it to be used by an embedded component.

PR Close #19682
2017-10-13 09:18:42 -07:00
c3f07b329f feat(aio): allow SearchService to have multiple clients (#19682)
PR Close #19682
2017-10-13 09:18:41 -07:00
6121083ba5 test(aio): tidy up e2e tests that used invalid URLs (#19682)
PR Close #19682
2017-10-13 09:18:41 -07:00
717c68089d build(aio): remove unused imports and local variables (#19682)
PR Close #19682
2017-10-13 09:18:41 -07:00
09b4244baf fix(aio): upgrade rxjs dependency to work with TS 2.4 (#19682)
PR Close #19682
2017-10-13 09:18:41 -07:00
2e45267705 ci: add service-worker to pullapprove (#19642)
PR Close #19642
2017-10-13 09:15:59 -07:00
e81d1fc361 docs(animations): add missing bracket to fadeAnimation
Closes #18899
2017-10-13 09:12:36 -07:00
02394d2d80 fix(core): don't refer to hydration in docs anymore.
Closes #18458
2017-10-13 08:53:44 -07:00
653a211743 Revert "Revert "Revert "perf(compiler): skip type check and emit in bazel in some cases. (#19646)"""
This reverts commit 6b7cead0c5.
2017-10-12 16:09:49 -07:00
189 changed files with 3798 additions and 3244 deletions

View File

@ -29,7 +29,7 @@ jobs:
- restore_cache:
key: angular-{{ .Branch }}-{{ checksum "yarn.lock" }}
- run: yarn install --freeze-lockfile --non-interactive
- run: yarn install --frozen-lockfile --non-interactive
- run: ./node_modules/.bin/gulp lint
build:
@ -42,6 +42,7 @@ jobs:
- run: bazel run @yarn//:yarn
- run: bazel build packages/...
- run: bazel test @angular//...
- save_cache:
key: angular-{{ .Branch }}-{{ checksum "yarn.lock" }}
paths:

View File

@ -7,9 +7,12 @@
#
# alexeagle - Alex Eagle
# alxhub - Alex Rickabaugh
# brocco - Mike Brocchi
# chuckjaz - Chuck Jazdzewski
# filipesilva - Filipe Silva
# Foxandxss - Jesús Rodríguez
# gkalpak - George Kalpakas
# hansl - Hans Larsen
# IgorMinar - Igor Minar
# jasonaden - Jason Aden
# juleskremer - Jules Kremer
@ -101,7 +104,6 @@ groups:
- vicb
- IgorMinar #fallback
core:
conditions:
files:
@ -116,7 +118,7 @@ groups:
animations:
conditions:
files:
- "packages/animation/*"
- "packages/animations/*"
- "packages/platform-browser/animations/*"
users:
- matsko #primary
@ -145,11 +147,23 @@ groups:
- mhevery
- IgorMinar #fallback
compiler-cli/ngtools:
conditions:
files:
- "packages/compiler-cli/src/ngtools*"
users:
- hansl
- filipesilva #fallback
- brocco #fallback
compiler-cli:
conditions:
files:
- "packages/compiler-cli/*"
- "packages/bazel/*"
include:
- "packages/compiler-cli/*"
- "packages/bazel/*"
exclude:
- "packages/compiler-cli/src/ngtools*"
users:
- alexeagle
- chuckjaz
@ -255,7 +269,15 @@ groups:
- IgorMinar #fallback
- mhevery #fallback
service-worker:
conditions:
files:
- "packages/service-worker/*"
users:
- alxhub #primary
- gkalpak
- IgorMinar #fallback
- mhevery #fallback
benchpress:
conditions:

View File

@ -1,12 +1,10 @@
language: node_js
sudo: false
# force trusty as Google Chrome addon is not supported on Precise
dist: trusty
node_js:
- '6.9.5'
addons:
chrome: stable
# firefox: "38.0"
apt:
sources:
@ -50,7 +48,8 @@ env:
- CI_MODE=e2e_2
- CI_MODE=js
- CI_MODE=saucelabs_required
- CI_MODE=browserstack_required
# deactivated, see #19768
# - CI_MODE=browserstack_required
- CI_MODE=saucelabs_optional
- CI_MODE=browserstack_optional
- CI_MODE=aio_tools_test

View File

@ -12,6 +12,7 @@ filegroup(
# This won't scale in the general case.
# TODO(alexeagle): figure out what to do
srcs = glob(["/".join(["node_modules", pkg, "**", ext]) for pkg in [
"jasmine",
"typescript",
"zone.js",
"rxjs",

View File

@ -1,3 +1,94 @@
<a name="5.0.0-rc.7"></a>
# [5.0.0-rc.7](https://github.com/angular/angular/compare/5.0.0-rc.6...5.0.0-rc.7) (2017-10-27)
### Bug Fixes
* **compiler:** dont store invalid state when using `listLazyRoutes` ([#19953](https://github.com/angular/angular/issues/19953)) ([4a23df3](https://github.com/angular/angular/commit/4a23df3))
* **compiler:** make watch mode work on windows ([#19953](https://github.com/angular/angular/issues/19953)) ([f4d5729](https://github.com/angular/angular/commit/f4d5729)), closes [#19951](https://github.com/angular/angular/issues/19951)
* **compiler:** recover from structural errors in watch mode ([#19953](https://github.com/angular/angular/issues/19953)) ([d343bf7](https://github.com/angular/angular/commit/d343bf7))
* **compiler:** translate emit diagnostics with `noEmitOnError: true`. ([#19953](https://github.com/angular/angular/issues/19953)) ([9ce7f0e](https://github.com/angular/angular/commit/9ce7f0e)), closes [#19935](https://github.com/angular/angular/issues/19935)
* **service-worker:** don't block initialization on registration ([#19936](https://github.com/angular/angular/issues/19936)) ([47caebf](https://github.com/angular/angular/commit/47caebf))
* **service-worker:** fix improper call of Observable.merge ([#19962](https://github.com/angular/angular/issues/19962)) ([14016c7](https://github.com/angular/angular/commit/14016c7))
* **service-worker:** listen for messages on the right event source ([#19954](https://github.com/angular/angular/issues/19954)) ([5cfd9c6](https://github.com/angular/angular/commit/5cfd9c6))
<a name="5.0.0-rc.6"></a>
# [5.0.0-rc.6](https://github.com/angular/angular/compare/5.0.0-rc.5...5.0.0-rc.6) (2017-10-25)
### Bug Fixes
* **compiler:** automatically set `emitDecoratorMetadata` when `"annotationsAs": "static fields”` ([#19927](https://github.com/angular/angular/issues/19927)) ([ef08330](https://github.com/angular/angular/commit/ef08330)), closes [#19916](https://github.com/angular/angular/issues/19916)
* **compiler:** dont type check templates with `skipTemplateCodegen` ([#19909](https://github.com/angular/angular/issues/19909)) ([18bce59](https://github.com/angular/angular/commit/18bce59))
* **compiler-cli:** only use error collector when needed. ([#19912](https://github.com/angular/angular/issues/19912)) ([9b26455](https://github.com/angular/angular/commit/9b26455))
* **compiler-cli:** produce correct paths for windows output ([#19915](https://github.com/angular/angular/issues/19915)) ([6cc042e](https://github.com/angular/angular/commit/6cc042e))
<a name="5.0.0-rc.5"></a>
# [5.0.0-rc.5](https://github.com/angular/angular/compare/5.0.0-rc.4...5.0.0-rc.5) (2017-10-24)
### Bug Fixes
* **compiler-cli:** report all diagnostic error messages ([#19886](https://github.com/angular/angular/issues/19886)) ([a82f863](https://github.com/angular/angular/commit/a82f863))
<a name="5.0.0-rc.4"></a>
# [5.0.0-rc.4](https://github.com/angular/angular/compare/5.0.0-rc.3...5.0.0-rc.4) (2017-10-24)
### Bug Fixes
* **bazel:** don't console.error from the compile helper ([#19879](https://github.com/angular/angular/issues/19879)) ([5da96c7](https://github.com/angular/angular/commit/5da96c7))
* **compiler:** correctly calculate the outDir if it repeats a parts of the `rootDir`. ([#19836](https://github.com/angular/angular/issues/19836)) ([fc0b1d5](https://github.com/angular/angular/commit/fc0b1d5)), closes [#19718](https://github.com/angular/angular/issues/19718)
* **service-worker:** include versionedFiles in the manifest hashTable ([#19837](https://github.com/angular/angular/issues/19837)) ([90d1423](https://github.com/angular/angular/commit/90d1423))
<a name="5.0.0-rc.3"></a>
# [5.0.0-rc.3](https://github.com/angular/angular/compare/5.0.0-rc.2...5.0.0-rc.3) (2017-10-18)
### Bug Fixes
* **animations:** always fire inner trigger callbacks even if blocked by parent animations ([#19753](https://github.com/angular/angular/issues/19753)) ([5a9ed2d](https://github.com/angular/angular/commit/5a9ed2d)), closes [#19100](https://github.com/angular/angular/issues/19100)
* **animations:** ensure animateChild() works with all inner leave animations ([#19006](https://github.com/angular/angular/issues/19006)) ([#19532](https://github.com/angular/angular/issues/19532)) ([#19693](https://github.com/angular/angular/issues/19693)) ([f42d317](https://github.com/angular/angular/commit/f42d317))
* **animations:** ensure inner :leave animations do not remove node when skipped ([#19532](https://github.com/angular/angular/issues/19532)) ([#19693](https://github.com/angular/angular/issues/19693)) ([d035175](https://github.com/angular/angular/commit/d035175))
* **bazel:** fix the output directory for extractor to be genfiles/ instead of bin/ ([#19716](https://github.com/angular/angular/issues/19716)) ([405ccc7](https://github.com/angular/angular/commit/405ccc7))
* **common:** attempt to JSON.parse errors for JSON responses ([#19773](https://github.com/angular/angular/issues/19773)) ([04ab9f1](https://github.com/angular/angular/commit/04ab9f1))
* **compiler:** generate correct imports for type check blocks ([#19582](https://github.com/angular/angular/issues/19582)) ([60bdcd6](https://github.com/angular/angular/commit/60bdcd6))
* **compiler:** prepare for future Bazel semantics of += ([#19717](https://github.com/angular/angular/issues/19717)) ([836c889](https://github.com/angular/angular/commit/836c889))
* **compiler-cli:** diagnostics file paths relative to cwd, not tsconfig ([#19748](https://github.com/angular/angular/issues/19748)) ([56774df](https://github.com/angular/angular/commit/56774df))
* **compiler-cli:** do not add references to files outside of `rootDir` ([#19770](https://github.com/angular/angular/issues/19770)) ([25cbc98](https://github.com/angular/angular/commit/25cbc98))
* **router:** RouterLinkActive should update its state right after checking the children ([#19449](https://github.com/angular/angular/issues/19449)) ([6f2939d](https://github.com/angular/angular/commit/6f2939d)), closes [#18983](https://github.com/angular/angular/issues/18983)
* **service-worker:** add missing annotation for SwPush ([#19721](https://github.com/angular/angular/issues/19721)) ([15a8429](https://github.com/angular/angular/commit/15a8429))
* **service-worker:** freshness strategy should clone response for cache ([#19764](https://github.com/angular/angular/issues/19764)) ([396c241](https://github.com/angular/angular/commit/396c241))
* **service-worker:** PushEvent.data has to be decoded ([#19764](https://github.com/angular/angular/issues/19764)) ([3bcf0cf](https://github.com/angular/angular/commit/3bcf0cf))
* **service-worker:** use posix path resolution for generation of ngsw.json ([#19527](https://github.com/angular/angular/issues/19527)) ([621f87b](https://github.com/angular/angular/commit/621f87b))
<a name="4.4.6"></a>
## [4.4.6](https://github.com/angular/angular/compare/4.4.5...4.4.6) (2017-10-18)
### Bug Fixes
* **animations:** properly support boolean-based transitions and state changes ([#19672](https://github.com/angular/angular/issues/19672)) ([f983a6c](https://github.com/angular/angular/commit/f983a6c)), closes [#9396](https://github.com/angular/angular/issues/9396) [#12337](https://github.com/angular/angular/issues/12337)
* **common:** attempt to JSON.parse errors for JSON responses ([#19773](https://github.com/angular/angular/issues/19773)) ([269f5ac](https://github.com/angular/angular/commit/269f5ac))
* **router:** RouterLinkActive should update its state right after checking the children ([53a807a](https://github.com/angular/angular/commit/53a807a)), closes [#18983](https://github.com/angular/angular/issues/18983)
### Performance Improvements
* **animations:** reduce size of bundle by removing AST classes ([#19673](https://github.com/angular/angular/issues/19673)) ([76d2496](https://github.com/angular/angular/commit/76d2496))
<a name="5.0.0-rc.2"></a>
# [5.0.0-rc.2](https://github.com/angular/angular/compare/5.0.0-rc.1...5.0.0-rc.2) (2017-10-12)
@ -119,7 +210,7 @@
### BREAKING CHANGES
* **compiler:** The method `ngGetConentSelectors()`, deprecated in Angular 4.0, has been removed.
* **compiler:** The method `ngGetContentSelectors()`, deprecated in Angular 4.0, has been removed.
Use `ComponentFactory.ngContentSelectors` instead.

View File

@ -220,6 +220,7 @@ The following is the list of supported scopes:
* **platform-webworker**
* **platform-webworker-dynamic**
* **router**
* **service-worker**
* **upgrade**
There are currently a few exceptions to the "use package name" rule:

View File

@ -5,7 +5,8 @@ load("@bazel_tools//tools/build_defs/repo:git.bzl", "git_repository")
git_repository(
name = "build_bazel_rules_nodejs",
remote = "https://github.com/bazelbuild/rules_nodejs.git",
tag = "0.1.6",
# TODO(alexeagle): use the correct tag here.
commit = "2c6243df53fd33fdab283ebdd01582e4eb815db8",
)
load("@build_bazel_rules_nodejs//:defs.bzl", "node_repositories")
@ -20,4 +21,4 @@ local_repository(
local_repository(
name = "angular",
path = "packages/bazel",
)
)

View File

@ -156,7 +156,7 @@ RUN find $AIO_SCRIPTS_SH_DIR -maxdepth 1 -type f -printf "%P\n" \
# Set up the Node.js scripts
COPY scripts-js/ $AIO_SCRIPTS_JS_DIR/
WORKDIR $AIO_SCRIPTS_JS_DIR/
RUN yarn install --production --freeze-lockfile
RUN yarn install --production --frozen-lockfile
# Set up health check

View File

@ -9,7 +9,7 @@ readonly defaultImageNameAndTag="aio-builds:latest"
# (Necessary, because only `scripts-js/dist/` is copied to the docker image.)
(
cd "$SCRIPTS_JS_DIR"
yarn install --freeze-lockfile --non-interactive
yarn install --frozen-lockfile --non-interactive
yarn build
)

View File

@ -7,6 +7,6 @@ source "`dirname $0`/_env.sh"
# Test `scripts-js/`
(
cd "$SCRIPTS_JS_DIR"
yarn install --freeze-lockfile --non-interactive
yarn install --frozen-lockfile --non-interactive
yarn test
)

View File

@ -12,8 +12,10 @@ describe('Component Style Tests', function () {
let componentH1 = element(by.css('app-root > h1'));
let externalH1 = element(by.css('body > h1'));
expect(componentH1.getCssValue('fontWeight')).toEqual('normal');
expect(externalH1.getCssValue('fontWeight')).not.toEqual('normal');
// Note: sometimes webdriver returns the fontWeight as "normal",
// othertimes as "400", both of which are equal in CSS terms.
expect(componentH1.getCssValue('fontWeight')).toMatch(/normal|400/);
expect(externalH1.getCssValue('fontWeight')).not.toMatch(/normal|400/);
});

View File

@ -7,3 +7,4 @@
<p>We're sorry. The page you are looking for cannot be found.</p>
</div>
</div>
<aio-file-not-found-search></aio-file-not-found-search>

View File

@ -4502,8 +4502,8 @@ helps instantly identify which members of the component serve which purpose.
**Why?** The property associated with `@HostBinding` or the method associated with `@HostListener`
can be modified only in a single place&mdash;in the directive's class.
If you use the `host` metadata property, you must modify both the property declaration inside the controller,
and the metadata associated with the directive.
If you use the `host` metadata property, you must modify both the property/method declaration in the
directive's class and the metadata in the decorator associated with the directive.
</div>

View File

@ -13,18 +13,6 @@
</tr>
</thead>
<tbody>
<!-- ngJapan -->
<tr>
<th><a href="http://ngjapan.org/" title="ng-Japan">ng-Japan</a></th>
<td>Tokyo, Japan</td>
<td>June 17, 2017</td>
</tr>
<!-- AngularMix -->
<tr>
<th><a href="https://angularmix.com/" title="AngularMix">AngularMix</a></th>
<td>Universal Studios, Orlando, Florida</td>
<td>October 8, 2017</td>
</tr>
<!-- ReactiveConf -->
<tr>
<th><a href="https://reactiveconf.com/" title="ReactiveConf">ReactiveConf</a></th>
@ -35,7 +23,7 @@
<tr>
<th><a href="http://angularconnect.com" title="AngularConnect">AngularConnect</a></th>
<td>London, United Kingdom</td>
<td>November 07, 2017</td>
<td>November 7-8, 2017</td>
</tr>
<!-- ngAtlanta-->
<tr>
@ -43,11 +31,30 @@
<td>Atlanta, Georgia</td>
<td>January 30, 2018</td>
</tr>
<!-- ngVikings-->
<tr>
<th><a href="https://ngvikings.org/" title="ngVikings">ngVikings</a></th>
<td>Helsinki, Finland</td>
<td>March 1-2, 2018</td>
</tr>
<!-- ngconf 2018-->
<tr>
<th><a href="https://www.ng-conf.org/" title="ng-conf">ng-conf</a></th>
<td>Salt Lake City, UT</td>
<td>April 18-20, 2018</td>
</tr>
<!-- WeRDevs-->
<tr>
<th><a href="https://www.wearedevelopers.com/" title="WeAreDevs">WeAreDevelopers</a></th>
<td>Vienna</td>
<td>May 16-18, 2018</td>
</tr>
<!-- AngularConnect-->
<tr>
<th><a href="http://angularconnect.com" title="AngularConnect">AngularConnect</a></th>
<td>London, United Kingdom</td>
<td>November 5-7, 2018</td>
</tr>
</tbody>
</table>
</article>

View File

@ -1,4 +1,4 @@
import { browser, element, by, promise } from 'protractor';
import { element, by } from 'protractor';
import { SitePage } from './app.po';
describe('site App', function() {
@ -41,20 +41,20 @@ describe('site App', function() {
describe('scrolling to the top', () => {
it('should scroll to the top when navigating to another page', () => {
page.navigateTo('guide/docs');
page.navigateTo('guide/security');
page.scrollToBottom();
page.getScrollTop().then(scrollTop => expect(scrollTop).toBeGreaterThan(0));
page.navigateTo('guide/api');
page.navigateTo('api');
page.getScrollTop().then(scrollTop => expect(scrollTop).toBe(0));
});
it('should scroll to the top when navigating to the same page', () => {
page.navigateTo('guide/docs');
page.navigateTo('guide/security');
page.scrollToBottom();
page.getScrollTop().then(scrollTop => expect(scrollTop).toBeGreaterThan(0));
page.navigateTo('guide/docs');
page.navigateTo('guide/security');
page.getScrollTop().then(scrollTop => expect(scrollTop).toBe(0));
});
});
@ -66,30 +66,35 @@ describe('site App', function() {
});
});
// TODO(https://github.com/angular/angular/issues/19785): Activate this again
// once it is no more flaky.
describe('google analytics', () => {
beforeEach(done => page.gaReady.then(done));
it('should call ga', done => {
page.ga()
.then(calls => {
expect(calls.length).toBeGreaterThan(2, 'ga calls');
done();
});
});
it('should call ga with initial URL', done => {
let path: string;
page.navigateTo('api');
page.locationPath()
.then(p => path = p)
.then(() => page.ga().then(calls => {
expect(calls.length).toBeGreaterThan(2, 'ga calls');
expect(calls[1]).toEqual(['set', 'page', path]);
// The last call (length-1) will be the `send` command
// The second to last call (length-2) will be the command to `set` the page url
expect(calls[calls.length - 2]).toEqual(['set', 'page', path]);
done();
}));
});
// Todo: add test to confirm tracking URL when navigate.
it('should call ga with new URL on navigation', done => {
let path: string;
page.getLink('features').click();
page.locationPath()
.then(p => path = p)
.then(() => page.ga().then(calls => {
// The last call (length-1) will be the `send` command
// The second to last call (length-2) will be the command to `set` the page url
expect(calls[calls.length - 2]).toEqual(['set', 'page', path]);
done();
}));
});
});
describe('search', () => {
@ -100,4 +105,12 @@ describe('site App', function() {
expect(page.getSearchResults().map(link => link.getText())).toContain('ControlValueAccessor');
});
});
describe('404 page', () => {
it('should search the index for words found in the url', () => {
page.navigateTo('http/router');
expect(page.getSearchResults().map(link => link.getText())).toContain('Http');
expect(page.getSearchResults().map(link => link.getText())).toContain('Router');
});
});
});

View File

@ -12,7 +12,6 @@ export class SitePage {
.all(by.css('a'))
.filter((a: ElementFinder) => a.getAttribute('href').then(href => githubRegex.test(href)))
.first();
gaReady: promise.Promise<any>;
static setWindowWidth(newWidth: number) {
const win = browser.driver.manage().window();
@ -25,11 +24,14 @@ export class SitePage {
.first();
}
getLink(path) { return element(by.css(`a[href="${path}"]`)); }
ga() { return browser.executeScript('return window["gaCalls"]') as promise.Promise<any[][]>; }
ga() { return browser.executeScript('return window["ga"].q') as promise.Promise<any[][]>; }
locationPath() { return browser.executeScript('return document.location.pathname') as promise.Promise<string>; }
navigateTo(pageUrl = '') {
return browser.get('/' + pageUrl).then(_ => this.replaceGa(_));
return browser.get('/' + pageUrl)
// We need to tell the index.html not to load the real analytics library
// See the GA snippet in index.html
.then(() => browser.driver.executeScript('sessionStorage.setItem("__e2e__", true);'));
}
getDocViewerText() {
@ -61,31 +63,4 @@ export class SitePage {
browser.wait(ExpectedConditions.presenceOf(results.first()), 8000);
return results;
}
/**
* Replace the ambient Google Analytics tracker with homebrew spy
* don't send commands to GA during e2e testing!
* @param _ - forward's anything passed in
*/
private replaceGa(_: any) {
this.gaReady = browser.driver.executeScript(() => {
// Give ga() a "ready" callback:
// https://developers.google.com/analytics/devguides/collection/analyticsjs/command-queue-reference
window['ga'](() => {
window['gaCalls'] = [];
window['ga'] = function() { window['gaCalls'].push(arguments); };
});
})
.then(() => {
// wait for GaService to start using window.ga after analytics lib loads.
const d = promise.defer();
setTimeout(() => d.fulfill(), 1000); // GaService.initializeDelay
return d.promise;
});
return _;
}
}

View File

@ -44,6 +44,17 @@
"source": "**/!(*.*)",
"destination": "/index.html"
}
],
"headers": [
{
"source": "/",
"headers": [
{
"key": "Link",
"value": "</generated/navigation.json>;rel=preload;as=fetch,</generated/docs/index.json>;rel=preload;as=fetch"
}
]
}
]
}
}

View File

@ -7,7 +7,7 @@
"license": "MIT",
"scripts": {
"aio-use-local": "node tools/ng-packages-installer overwrite . --debug --ignore-packages @angular/service-worker",
"aio-use-npm": "node tools/ng-packages-installer restore .",
"aio-use-npm": "node tools/ng-packages-installer restore . && yarn upgrade @angular/cli@1.3.0",
"aio-check-local": "node tools/ng-packages-installer check .",
"ng": "yarn check-env && ng",
"start": "yarn check-env && ng serve",
@ -19,11 +19,11 @@
"test": "yarn check-env && ng test",
"pree2e": "yarn check-env && yarn ~~update-webdriver",
"e2e": "ng e2e --no-webdriver-update",
"presetup": "yarn install --freeze-lockfile && yarn ~~check-env && yarn boilerplate:remove",
"presetup": "yarn install --frozen-lockfile && yarn ~~check-env && yarn boilerplate:remove",
"setup": "yarn aio-use-npm && yarn example-use-npm",
"postsetup": "yarn boilerplate:add && yarn build-ie-polyfills && yarn generate-plunkers && yarn generate-zips && yarn docs",
"presetup-local": "yarn presetup",
"setup-local": "yarn aio-use-local && yarn example-use-local",
"setup-local": "yarn aio-use-local && yarn upgrade @angular/cli@1.5.0-rc.2 && yarn example-use-local",
"postsetup-local": "yarn postsetup",
"pretest-pwa-score-localhost": "yarn build",
"test-pwa-score-localhost": "concurrently --kill-others --success first \"http-server dist -p 4200 --silent\" \"yarn test-pwa-score http://localhost:4200 90\"",
@ -43,7 +43,7 @@
"docs-watch": "node tools/transforms/authors-package/watchr.js",
"docs-lint": "eslint --ignore-path=\"tools/transforms/.eslintignore\" tools/transforms",
"docs-test": "node tools/transforms/test.js",
"tools-test": "./scripts/deploy-to-firebase.test.sh && yarn docs-test && yarn boilerplate:test && jasmine tools/ng-packages-installer.spec.js",
"tools-test": "./scripts/deploy-to-firebase.test.sh && yarn docs-test && yarn boilerplate:test && jasmine tools/ng-packages-installer/index.spec.js",
"serve-and-sync": "concurrently --kill-others \"yarn docs-watch\" \"yarn start\"",
"boilerplate:add": "node ./tools/examples/example-boilerplate add",
"boilerplate:remove": "node ./tools/examples/example-boilerplate remove",

View File

@ -4,7 +4,7 @@ set -u -e -o pipefail
declare -A payloadLimits
payloadLimits["aio", "uncompressed", "inline"]=1600
payloadLimits["aio", "uncompressed", "main"]=525000
payloadLimits["aio", "uncompressed", "main"]=525500
payloadLimits["aio", "uncompressed", "polyfills"]=38000
payloadLimits["aio", "gzip7", "inline"]=1000
payloadLimits["aio", "gzip7", "main"]=127000

View File

@ -19,6 +19,11 @@ const config = require('lighthouse/lighthouse-core/config/default.js');
// Constants
const VIEWER_URL = 'https://googlechrome.github.io/lighthouse/viewer/';
// Specify the path to Chrome on Travis
if (process.env.TRAVIS) {
process.env.LIGHTHOUSE_CHROMIUM_PATH = process.env.CHROME_BIN;
}
// Run
_main(process.argv.slice(2));

View File

@ -13,7 +13,7 @@
<aio-top-menu *ngIf="isSideBySide" [nodes]="topMenuNodes"></aio-top-menu>
<aio-search-box class="search-container" #searchBox (onSearch)="doSearch($event)" (onFocus)="doSearch($event)"></aio-search-box>
</md-toolbar>
<aio-search-results #searchResults *ngIf="showSearchResults" (resultSelected)="hideSearchResults()"></aio-search-results>
<aio-search-results #searchResultsView *ngIf="showSearchResults" [searchResults]="searchResults | async" (resultSelected)="hideSearchResults()"></aio-search-results>
<md-sidenav-container class="sidenav-container" [class.starting]="isStarting" [class.has-floating-toc]="hasFloatingToc" role="main">

View File

@ -1,12 +1,11 @@
import { NO_ERRORS_SCHEMA, DebugElement } from '@angular/core';
import { async, inject, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
import { inject, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
import { Title } from '@angular/platform-browser';
import { APP_BASE_HREF } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { MdProgressBar, MdSidenav } from '@angular/material';
import { By } from '@angular/platform-browser';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { of } from 'rxjs/observable/of';
import { AppComponent } from './app.component';
@ -22,9 +21,9 @@ import { MockSearchService } from 'testing/search.service';
import { NavigationNode } from 'app/navigation/navigation.service';
import { ScrollService } from 'app/shared/scroll.service';
import { SearchBoxComponent } from 'app/search/search-box/search-box.component';
import { SearchResultsComponent } from 'app/search/search-results/search-results.component';
import { SearchResultsComponent } from 'app/shared/search-results/search-results.component';
import { SearchService } from 'app/search/search.service';
import { SelectComponent, Option } from 'app/shared/select/select.component';
import { SelectComponent } from 'app/shared/select/select.component';
import { TocComponent } from 'app/embedded/toc/toc.component';
import { TocItem, TocService } from 'app/shared/toc.service';
@ -1054,11 +1053,6 @@ class TestGaService {
locationChanged = jasmine.createSpy('locationChanged');
}
class TestSearchService {
initWorker = jasmine.createSpy('initWorker');
loadIndex = jasmine.createSpy('loadIndex');
}
class TestHttpClient {
static versionInfo = {

View File

@ -2,18 +2,18 @@ import { Component, ElementRef, HostBinding, HostListener, OnInit,
QueryList, ViewChild, ViewChildren } from '@angular/core';
import { MdSidenav } from '@angular/material';
import { CurrentNodes, NavigationService, NavigationViews, NavigationNode, VersionInfo } from 'app/navigation/navigation.service';
import { CurrentNodes, NavigationService, NavigationNode, VersionInfo } from 'app/navigation/navigation.service';
import { DocumentService, DocumentContents } from 'app/documents/document.service';
import { DocViewerComponent } from 'app/layout/doc-viewer/doc-viewer.component';
import { Deployment } from 'app/shared/deployment.service';
import { LocationService } from 'app/shared/location.service';
import { NavMenuComponent } from 'app/layout/nav-menu/nav-menu.component';
import { ScrollService } from 'app/shared/scroll.service';
import { SearchResultsComponent } from 'app/search/search-results/search-results.component';
import { SearchBoxComponent } from 'app/search/search-box/search-box.component';
import { SearchResults } from 'app/search/interfaces';
import { SearchService } from 'app/search/search.service';
import { TocService } from 'app/shared/toc.service';
import { Observable } from 'rxjs/Observable';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { combineLatest } from 'rxjs/observable/combineLatest';
@ -89,10 +89,9 @@ export class AppComponent implements OnInit {
// Search related properties
showSearchResults = false;
@ViewChildren('searchBox, searchResults', { read: ElementRef })
searchResults: Observable<SearchResults>;
@ViewChildren('searchBox, searchResultsView', { read: ElementRef })
searchElements: QueryList<ElementRef>;
@ViewChild(SearchResultsComponent)
searchResults: SearchResultsComponent;
@ViewChild(SearchBoxComponent)
searchBox: SearchBoxComponent;
@ -332,7 +331,7 @@ export class AppComponent implements OnInit {
}
doSearch(query) {
this.searchService.search(query);
this.searchResults = this.searchService.search(query);
this.showSearchResults = !!query;
}

View File

@ -43,9 +43,9 @@ import { NavMenuComponent } from 'app/layout/nav-menu/nav-menu.component';
import { NavItemComponent } from 'app/layout/nav-item/nav-item.component';
import { ScrollService } from 'app/shared/scroll.service';
import { ScrollSpyService } from 'app/shared/scroll-spy.service';
import { SearchResultsComponent } from './search/search-results/search-results.component';
import { SearchBoxComponent } from './search/search-box/search-box.component';
import { TocService } from 'app/shared/toc.service';
import { WindowToken, windowProvider } from 'app/shared/window';
import { SharedModule } from 'app/shared/shared.module';
@ -95,7 +95,6 @@ export const svgIconProviders = [
ModeBannerComponent,
NavMenuComponent,
NavItemComponent,
SearchResultsComponent,
SearchBoxComponent,
TopMenuComponent,
],
@ -115,7 +114,8 @@ export const svgIconProviders = [
ScrollSpyService,
SearchService,
svgIconProviders,
TocService
TocService,
{ provide: WindowToken, useFactory: windowProvider },
],
bootstrap: [AppComponent]
})

View File

@ -1,10 +1,7 @@
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { Injector } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { Observable } from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { LocationService } from 'app/shared/location.service';
import { MockLocationService } from 'testing/location.service';

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { ApiListComponent } from './api-list.component';

View File

@ -13,7 +13,7 @@ import { ReplaySubject } from 'rxjs/ReplaySubject';
import { combineLatest } from 'rxjs/observable/combineLatest';
import { LocationService } from 'app/shared/location.service';
import { ApiItem, ApiSection, ApiService } from './api.service';
import { ApiSection, ApiService } from './api.service';
import { Option } from 'app/shared/select/select.component';

View File

@ -1,6 +1,6 @@
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { Injector } from '@angular/core';
import { TestBed, inject } from '@angular/core/testing';
import { TestBed } from '@angular/core/testing';
import { Logger } from 'app/shared/logger.service';

View File

@ -1,11 +1,9 @@
import { Component, ElementRef, ViewChild, OnChanges, OnDestroy, Input } from '@angular/core';
import { Component, ElementRef, ViewChild, OnChanges, Input } from '@angular/core';
import { Logger } from 'app/shared/logger.service';
import { PrettyPrinter } from './pretty-printer.service';
import { CopierService } from 'app/shared/copier.service';
import { MdSnackBar } from '@angular/material';
const originalLabel = 'Copy Code';
const copiedLabel = 'Copied!';
const defaultLineNumsCount = 10; // by default, show linenums over this number
/**

View File

@ -3,7 +3,7 @@ import { Injector } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { ContributorService } from './contributor.service';
import { Contributor, ContributorGroup } from './contributors.model';
import { ContributorGroup } from './contributors.model';
describe('ContributorService', () => {

View File

@ -1,4 +1,4 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LocationService } from 'app/shared/location.service';
import { MockLocationService } from 'testing/location.service';
import { CurrentLocationComponent } from './current-location.component';

View File

@ -20,6 +20,7 @@ import { CodeTabsComponent } from './code/code-tabs.component';
import { ContributorListComponent } from './contributor/contributor-list.component';
import { ContributorComponent } from './contributor/contributor.component';
import { CurrentLocationComponent } from './current-location.component';
import { FileNotFoundSearchComponent } from './search/file-not-found-search.component';
import { LiveExampleComponent, EmbeddedPlunkerComponent } from './live-example/live-example.component';
import { ResourceListComponent } from './resource/resource-list.component';
import { ResourceService } from './resource/resource.service';
@ -30,7 +31,8 @@ import { TocComponent } from './toc/toc.component';
*/
export const embeddedComponents: any[] = [
ApiListComponent, CodeExampleComponent, CodeTabsComponent, ContributorListComponent,
CurrentLocationComponent, LiveExampleComponent, ResourceListComponent, TocComponent
CurrentLocationComponent, FileNotFoundSearchComponent, LiveExampleComponent, ResourceListComponent,
TocComponent
];
/** Injectable class w/ property returning components that can be embedded in docs */

View File

@ -1,6 +1,6 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { Component, DebugElement, ElementRef } from '@angular/core';
import { Component, DebugElement } from '@angular/core';
import { Location } from '@angular/common';
import { LiveExampleComponent, EmbeddedPlunkerComponent } from './live-example.component';
@ -71,7 +71,6 @@ describe('LiveExampleComponent', () => {
describe('when not embedded', () => {
function getLiveExampleAnchor() { return getAnchors()[0]; }
function getDownloadAnchor() { return getAnchors()[1]; }
it('should create LiveExampleComponent', () => {
testComponent(() => {

View File

@ -1,7 +1,6 @@
import { ReflectiveInjector } from '@angular/core';
import { PlatformLocation } from '@angular/common';
import { Observable } from 'rxjs/Observable';
import { of } from 'rxjs/observable/of';
import { ResourceListComponent } from './resource-list.component';

View File

@ -3,7 +3,7 @@ import { Injector } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { ResourceService } from './resource.service';
import { Category, SubCategory, Resource } from './resource.model';
import { Category } from './resource.model';
describe('ResourceService', () => {

View File

@ -0,0 +1,49 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { Subject } from 'rxjs/Subject';
import { LocationService } from 'app/shared/location.service';
import { MockLocationService } from 'testing/location.service';
import { SearchResults } from 'app/search/interfaces';
import { SearchResultsComponent } from 'app/shared/search-results/search-results.component';
import { SearchService } from 'app/search/search.service';
import { FileNotFoundSearchComponent } from './file-not-found-search.component';
describe('FileNotFoundSearchComponent', () => {
let element: HTMLElement;
let fixture: ComponentFixture<FileNotFoundSearchComponent>;
let searchService: SearchService;
let searchResultSubject: Subject<SearchResults>;
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ FileNotFoundSearchComponent, SearchResultsComponent ],
providers: [
{ provide: LocationService, useValue: new MockLocationService('base/initial-url?some-query') },
SearchService
]
});
fixture = TestBed.createComponent(FileNotFoundSearchComponent);
searchService = TestBed.get(SearchService);
searchResultSubject = new Subject<SearchResults>();
spyOn(searchService, 'search').and.callFake(() => searchResultSubject.asObservable());
fixture.detectChanges();
element = fixture.nativeElement;
});
it('should run a search with a query built from the current url', () => {
expect(searchService.search).toHaveBeenCalledWith('base initial url');
});
it('should pass through any results to the `aio-search-results` component', () => {
const searchResultsComponent = fixture.debugElement.query(By.directive(SearchResultsComponent)).componentInstance;
expect(searchResultsComponent.searchResults).toBe(null);
const results = { query: 'base initial url', results: []};
searchResultSubject.next(results);
fixture.detectChanges();
expect(searchResultsComponent.searchResults).toEqual(results);
});
});

View File

@ -0,0 +1,23 @@
import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { LocationService } from 'app/shared/location.service';
import { SearchResults } from 'app/search/interfaces';
import { SearchService } from 'app/search/search.service';
@Component({
selector: 'aio-file-not-found-search',
template:
`<p>Let's see if any of these search results help...</p>
<aio-search-results class="embedded" [searchResults]="searchResults | async"></aio-search-results>`
})
export class FileNotFoundSearchComponent implements OnInit {
searchResults: Observable<SearchResults>;
constructor(private location: LocationService, private search: SearchService) {}
ngOnInit() {
this.searchResults = this.location.currentPath.switchMap(path => {
const query = path.split(/\W+/).join(' ');
return this.search.search(query);
});
}
}

View File

@ -1,6 +1,6 @@
import { Component, CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By, DOCUMENT } from '@angular/platform-browser';
import { By } from '@angular/platform-browser';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { asap } from 'rxjs/scheduler/asap';

View File

@ -1,11 +1,8 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import {
Component, ComponentFactoryResolver, DebugElement,
ElementRef, Injector, NgModule, OnInit, ViewChild } from '@angular/core';
import { By } from '@angular/platform-browser';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Component, DebugElement, ElementRef, NgModule, OnInit, ViewChild } from '@angular/core';
import { DocViewerComponent } from './doc-viewer.component';
import { DocumentContents } from 'app/documents/document.service';
import { EmbeddedModule, embeddedComponents, EmbeddedComponents } from 'app/embedded/embedded.module';
import { EmbeddedModule, EmbeddedComponents } from 'app/embedded/embedded.module';
import { Title } from '@angular/platform-browser';
import { TocService } from 'app/shared/toc.service';

View File

@ -1,7 +1,7 @@
import {
Component, ComponentFactory, ComponentFactoryResolver, ComponentRef,
DoCheck, ElementRef, EventEmitter, Injector, Input, OnDestroy,
Output, ViewEncapsulation
Output
} from '@angular/core';
import { EmbeddedComponents } from 'app/embedded/embedded.module';

View File

@ -1,6 +1,6 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { SimpleChange, SimpleChanges, NO_ERRORS_SCHEMA } from '@angular/core';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { NavItemComponent } from './nav-item.component';
import { NavigationNode } from 'app/navigation/navigation.model';

View File

@ -1,9 +1,9 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { TopMenuComponent } from './top-menu.component';
import { NavigationService, NavigationViews, NavigationNode } from 'app/navigation/navigation.service';
import { NavigationService, NavigationViews } from 'app/navigation/navigation.service';
describe('TopMenuComponent', () => {
let component: TopMenuComponent;

View File

@ -2,7 +2,6 @@ import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import { AsyncSubject } from 'rxjs/AsyncSubject';
import { combineLatest } from 'rxjs/observable/combineLatest';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/publishLast';

View File

@ -0,0 +1,19 @@
export interface SearchResults {
query: string;
results: SearchResult[];
}
export interface SearchResult {
path: string;
title: string;
type: string;
titleWords: string;
keywords: string;
}
export interface SearchArea {
name: string;
pages: SearchResult[];
priorityPages: SearchResult[];
}

View File

@ -2,7 +2,6 @@ import { Component } from '@angular/core';
import { ComponentFixture, fakeAsync, inject, TestBed, tick } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { SearchBoxComponent } from './search-box.component';
import { MockSearchService } from 'testing/search.service';
import { LocationService } from 'app/shared/location.service';
import { MockLocationService } from 'testing/location.service';

View File

@ -1,6 +1,7 @@
import { ReflectiveInjector, NgZone } from '@angular/core';
import { fakeAsync, tick } from '@angular/core/testing';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import { SearchService } from './search.service';
import { WebWorkerClient } from 'app/shared/web-worker';
@ -36,27 +37,29 @@ describe('SearchService', () => {
describe('search', () => {
beforeEach(() => {
// We must initialize the service before calling search
service.initWorker('some/url', 100);
// We must initialize the service before calling connectSearches
service.initWorker('some/url', 1000);
// Simulate the index being ready so that searches get sent to the worker
(service as any).ready = Observable.of(true);
});
it('should trigger a `loadIndex` synchronously', () => {
service.search('some query');
it('should trigger a `loadIndex` synchronously (not waiting for the delay)', () => {
expect(mockWorker.sendMessage).not.toHaveBeenCalled();
service.search('some query').subscribe();
expect(mockWorker.sendMessage).toHaveBeenCalledWith('load-index');
});
it('should send a "query-index" message to the worker', () => {
service.search('some query');
service.search('some query').subscribe();
expect(mockWorker.sendMessage).toHaveBeenCalledWith('query-index', 'some query');
});
it('should push the response to the `searchResults` observable', () => {
it('should push the response to the returned observable', () => {
const mockSearchResults = { results: ['a', 'b'] };
let actualSearchResults;
(mockWorker.sendMessage as jasmine.Spy).and.returnValue(Observable.of(mockSearchResults));
let searchResults: any;
service.searchResults.subscribe(results => searchResults = results);
service.search('some query');
expect(searchResults).toEqual(mockSearchResults);
service.search('some query').subscribe(results => actualSearchResults = results);
expect(actualSearchResults).toEqual(mockSearchResults);
});
});
});

View File

@ -4,34 +4,21 @@ Use of this source code is governed by an MIT-style license that
can be found in the LICENSE file at http://angular.io/license
*/
import { NgZone, Injectable, Type } from '@angular/core';
import { NgZone, Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { ReplaySubject } from 'rxjs/ReplaySubject';
import 'rxjs/add/observable/race';
import 'rxjs/add/observable/timer';
import 'rxjs/add/operator/concatMap';
import 'rxjs/add/operator/publish';
import 'rxjs/add/operator/publishReplay';
import { WebWorkerClient } from 'app/shared/web-worker';
export interface SearchResults {
query: string;
results: SearchResult[];
}
export interface SearchResult {
path: string;
title: string;
type: string;
titleWords: string;
keywords: string;
}
import { SearchResults } from 'app/search/interfaces';
@Injectable()
export class SearchService {
private ready: Observable<boolean>;
private searchesSubject = new ReplaySubject<string>(1);
searchResults: Observable<SearchResults>;
private worker: WebWorkerClient;
constructor(private zone: NgZone) {}
/**
@ -43,36 +30,32 @@ export class SearchService {
* @param initDelay the number of milliseconds to wait before we load the WebWorker and generate the search index
*/
initWorker(workerUrl: string, initDelay: number) {
const searchResults = Observable
const ready = this.ready = Observable
// Wait for the initDelay or the first search
.race<any>(
Observable.timer(initDelay),
this.searchesSubject.first()
(this.searchesSubject as Observable<string>).first()
)
.concatMap(() => {
// Create the worker and load the index
const worker = WebWorkerClient.create(workerUrl, this.zone);
return worker.sendMessage('load-index').concatMap(() =>
// Once the index has loaded, switch to listening to the searches coming in
this.searchesSubject.switchMap((query) =>
// Each search gets switched to a web worker message, whose results are returned via an observable
worker.sendMessage<SearchResults>('query-index', query)
)
);
}).publish();
this.worker = WebWorkerClient.create(workerUrl, this.zone);
return this.worker.sendMessage<boolean>('load-index');
}).publishReplay(1);
// Connect to the observable to kick off the timer
searchResults.connect();
// Expose the connected observable to the rest of the world
this.searchResults = searchResults;
// Connect to the observable to kick off the timer
ready.connect();
return ready;
}
/**
* Send a search query to the index.
* The results will appear on the `searchResults` observable.
* Search the index using the given query and emit results on the observable that is returned.
* @param query The query to run against the index.
* @returns an observable collection of search results
*/
search(query: string) {
search(query: string): Observable<SearchResults> {
// Trigger the searches subject to override the init delay timer
this.searchesSubject.next(query);
// Once the index has loaded, switch to listening to the searches coming in.
return this.ready.concatMap(() => this.worker.sendMessage<SearchResults>('query-index', query));
}
}

View File

@ -1,5 +1,5 @@
import { MdIconRegistry } from '@angular/material';
import { CustomMdIconRegistry, SVG_ICONS, SvgIconInfo } from './custom-md-icon-registry';
import { CustomMdIconRegistry, SvgIconInfo } from './custom-md-icon-registry';
describe('CustomMdIconRegistry', () => {
it('should get the SVG element for a preloaded icon from the cache', () => {

View File

@ -1,130 +1,87 @@
import { ReflectiveInjector } from '@angular/core';
import { fakeAsync, tick } from '@angular/core/testing';
import { GaService } from 'app/shared/ga.service';
import { Logger } from 'app/shared/logger.service';
import { WindowToken } from 'app/shared/window';
describe('GaService', () => {
let gaSpy: jasmine.Spy;
let gaService: GaService;
let injector: ReflectiveInjector;
// filter for 'send' which communicates with server
// returns the url of the 'send pageview'
function gaSpySendCalls() {
let lastUrl: string;
return gaSpy.calls.all()
.reduce((acc, c) => {
const args = c.args;
if (args[0] === 'set') {
lastUrl = args[2];
} else if (args[0] === 'send') {
acc.push(lastUrl);
}
return acc;
}, []);
}
let gaSpy: jasmine.Spy;
let mockWindow: any;
beforeEach(() => {
injector = ReflectiveInjector.resolveAndCreate([
GaService,
{ provide: Logger, useClass: TestLogger }
]);
gaSpy = jasmine.createSpy('ga');
mockWindow = { ga: gaSpy };
injector = ReflectiveInjector.resolveAndCreate([GaService, { provide: WindowToken, useFactory: () => mockWindow }]);
gaService = injector.get(GaService);
});
describe('with ambient GA', () => {
let gaService: GaService;
beforeEach(fakeAsync(() => {
this.winGa = window['ga']; // remember current GA tracker just in case
// Replace Google Analytics tracker with spy after calling "ga ready" callback
window['ga'] = (fn: Function) => {
window['ga'] = gaSpy = jasmine.createSpy('ga');
fn();
tick(GaService.initializeDelay); // see GaService#initializeGa
};
gaService = injector.get(GaService);
}));
afterEach(() => {
window['ga'] = this.winGa;
});
it('should initialize ga with "create" when constructed', () => {
const first = gaSpy.calls.first().args;
expect(first[0]).toBe('create');
});
describe('#locationChanged(url)', () => {
it('should send page to url w/ leading slash', () => {
gaService.locationChanged('testUrl');
let args = gaSpy.calls.all()[1].args;
expect(args).toEqual(['set', 'page', '/testUrl']);
args = gaSpy.calls.all()[2].args;
expect(args).toEqual(['send', 'pageview']);
});
});
describe('#sendPage(url)', () => {
it('should set page to url w/ leading slash', () => {
gaService.sendPage('testUrl');
const args = gaSpy.calls.all()[1].args;
expect(args).toEqual(['set', 'page', '/testUrl']);
});
it('should send "pageview" ', () => {
gaService.sendPage('testUrl');
const args = gaSpy.calls.all()[2].args;
expect(args).toEqual(['send', 'pageview']);
});
it('should not send twice with same URL, back-to-back', () => {
gaService.sendPage('testUrl');
gaService.sendPage('testUrl');
expect(gaSpySendCalls()).toEqual(['/testUrl']);
});
it('should send twice with same URL, back-to-back, even when the hash changes', () => {
// Therefore it is up to caller NOT to call it when hash changes if this is unwanted.
// See LocationService and its specs
gaService.sendPage('testUrl#one');
gaService.sendPage('testUrl#two');
expect(gaSpySendCalls()).toEqual([
'/testUrl#one',
'/testUrl#two'
]);
});
it('should send same URL twice when other intervening URL', () => {
gaService.sendPage('testUrl');
gaService.sendPage('testUrl2');
gaService.sendPage('testUrl');
expect(gaSpySendCalls()).toEqual([
'/testUrl',
'/testUrl2',
'/testUrl'
]);
});
});
it('should initialize ga with "create" when constructed', () => {
const first = gaSpy.calls.first().args;
expect(first[0]).toBe('create');
});
describe('when no ambient GA', () => {
let gaService: GaService;
let logger: TestLogger;
it('should log with "create" when constructed', () => {
gaService = injector.get(GaService);
logger = injector.get(Logger);
expect(logger.log.calls.count()).toBe(1, 'logger.log should be called');
const first = logger.log.calls.first().args;
expect(first[0]).toBe('ga:');
expect(first[1][0]).toBe('create'); // first[1] is the array of args to ga()
describe('#locationChanged(url)', () => {
it('should send page to url w/ leading slash', () => {
gaService.locationChanged('testUrl');
expect(gaSpy).toHaveBeenCalledWith('set', 'page', '/testUrl');
expect(gaSpy).toHaveBeenCalledWith('send', 'pageview');
});
});
describe('#sendPage(url)', () => {
it('should set page to url w/ leading slash', () => {
gaService.sendPage('testUrl');
expect(gaSpy).toHaveBeenCalledWith('set', 'page', '/testUrl');
});
it('should send "pageview" ', () => {
gaService.sendPage('testUrl');
expect(gaSpy).toHaveBeenCalledWith('send', 'pageview');
});
it('should not send twice with same URL, back-to-back', () => {
gaService.sendPage('testUrl');
gaSpy.calls.reset();
gaService.sendPage('testUrl');
expect(gaSpy).not.toHaveBeenCalled();
});
it('should send again even if only the hash changes', () => {
// Therefore it is up to caller NOT to call it when hash changes if this is unwanted.
// See LocationService and its specs
gaService.sendPage('testUrl#one');
expect(gaSpy).toHaveBeenCalledWith('set', 'page', '/testUrl#one');
expect(gaSpy).toHaveBeenCalledWith('send', 'pageview');
gaSpy.calls.reset();
gaService.sendPage('testUrl#two');
expect(gaSpy).toHaveBeenCalledWith('set', 'page', '/testUrl#two');
expect(gaSpy).toHaveBeenCalledWith('send', 'pageview');
});
it('should send same URL twice when other intervening URL', () => {
gaService.sendPage('testUrl');
expect(gaSpy).toHaveBeenCalledWith('set', 'page', '/testUrl');
expect(gaSpy).toHaveBeenCalledWith('send', 'pageview');
gaSpy.calls.reset();
gaService.sendPage('testUrl2');
expect(gaSpy).toHaveBeenCalledWith('set', 'page', '/testUrl2');
expect(gaSpy).toHaveBeenCalledWith('send', 'pageview');
gaSpy.calls.reset();
gaService.sendPage('testUrl');
expect(gaSpy).toHaveBeenCalledWith('set', 'page', '/testUrl');
expect(gaSpy).toHaveBeenCalledWith('send', 'pageview');
});
});
it('should support replacing the `window.ga` function', () => {
const gaSpy2 = jasmine.createSpy('new ga');
mockWindow.ga = gaSpy2;
gaSpy.calls.reset();
gaService.sendPage('testUrl');
expect(gaSpy).not.toHaveBeenCalled();
expect(gaSpy2).toHaveBeenCalledWith('set', 'page', '/testUrl');
expect(gaSpy2).toHaveBeenCalledWith('send', 'pageview');
});
});
class TestLogger {
log = jasmine.createSpy('log');
}

View File

@ -1,7 +1,7 @@
import { Injectable } from '@angular/core';
import { Inject, Injectable } from '@angular/core';
import { environment } from '../../environments/environment';
import { Logger } from 'app/shared/logger.service';
import { WindowToken } from 'app/shared/window';
@Injectable()
/**
@ -10,15 +10,10 @@ import { Logger } from 'app/shared/logger.service';
* Associates data with a GA "property" from the environment (`gaId`).
*/
export class GaService {
// ms to wait before acquiring window.ga after analytics library loads
// empirically determined to allow time for e2e test setup
static initializeDelay = 1000;
private previousUrl: string;
private ga: (...rest: any[]) => void;
constructor(private logger: Logger) {
this.initializeGa();
constructor(@Inject(WindowToken) private window: Window) {
this.ga('create', environment['gaId'] , 'auto');
}
@ -34,27 +29,7 @@ export class GaService {
this.ga('send', 'pageview');
}
// These gyrations are necessary to make the service e2e testable
// and to disable ga tracking during e2e tests.
private initializeGa() {
const ga = window['ga'];
if (ga) {
// Queue commands until GA analytics script has loaded.
const gaQueue: any[][] = [];
this.ga = (...rest: any[]) => { gaQueue.push(rest); };
// Then send queued commands to either real or e2e test ga();
// after waiting to allow possible e2e test to replace global ga function
ga(() => setTimeout(() => {
// this.logger.log('GA fn:', window['ga'].toString());
this.ga = window['ga'];
gaQueue.forEach((command) => this.ga.apply(null, command));
}, GaService.initializeDelay));
} else {
// delegate `ga` calls to the logger if no ga installed
this.ga = (...rest: any[]) => { this.logger.log('ga:', rest); };
}
ga(...args) {
this.window['ga'](...args);
}
}

View File

@ -1,7 +1,6 @@
import { Injectable } from '@angular/core';
import { Location, PlatformLocation } from '@angular/common';
import { Observable } from 'rxjs/Observable';
import { ReplaySubject } from 'rxjs/ReplaySubject';
import 'rxjs/add/operator/do';

View File

@ -3,7 +3,7 @@ import { fakeAsync, tick } from '@angular/core/testing';
import { DOCUMENT } from '@angular/platform-browser';
import { ScrollService } from 'app/shared/scroll.service';
import { ScrollItem, ScrollSpiedElement, ScrollSpiedElementGroup, ScrollSpyInfo, ScrollSpyService } from 'app/shared/scroll-spy.service';
import { ScrollItem, ScrollSpiedElement, ScrollSpiedElementGroup, ScrollSpyService } from 'app/shared/scroll-spy.service';
describe('ScrollSpiedElement', () => {
@ -197,6 +197,7 @@ describe('ScrollSpyService', () => {
.and.callFake(() => actions.push('calibrate'));
expect(onResizeSpy).not.toHaveBeenCalled();
expect(calibrateSpy).not.toHaveBeenCalled();
scrollSpyService.spyOn([]);
expect(actions).toEqual(['onResize', 'calibrate']);

View File

@ -1,16 +1,12 @@
import { DebugElement } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
import { SearchService, SearchResult, SearchResults } from '../search.service';
import { SearchResultsComponent, SearchArea } from './search-results.component';
import { MockSearchService } from 'testing/search.service';
import { SearchResult } from 'app/search/interfaces';
import { SearchResultsComponent } from './search-results.component';
describe('SearchResultsComponent', () => {
let component: SearchResultsComponent;
let fixture: ComponentFixture<SearchResultsComponent>;
let searchResults: Subject<SearchResults>;
/** Get all text from component element */
function getText() { return fixture.debugElement.nativeElement.textContent; }
@ -38,27 +34,26 @@ describe('SearchResultsComponent', () => {
return l.title.toUpperCase() > r.title.toUpperCase() ? 1 : -1;
}
function setSearchResults(query: string, results: SearchResult[]) {
component.searchResults = {query, results};
component.ngOnChanges({});
fixture.detectChanges();
}
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [ SearchResultsComponent ],
providers: [
{ provide: SearchService, useFactory: () => new MockSearchService() }
]
declarations: [ SearchResultsComponent ]
});
});
beforeEach(() => {
fixture = TestBed.createComponent(SearchResultsComponent);
component = fixture.componentInstance;
searchResults = TestBed.get(SearchService).searchResults;
fixture.detectChanges();
});
it('should map the search results into groups based on their containing folder', () => {
const results = getTestResults(3);
searchResults.next({ query: '', results: results});
setSearchResults('', getTestResults(3));
expect(component.searchAreas).toEqual([
{ name: 'api', priorityPages: [
{ path: 'api/d', title: 'API D', type: '', keywords: '', titleWords: '' }
@ -71,10 +66,10 @@ describe('SearchResultsComponent', () => {
});
it('should special case results that are top level folders', () => {
searchResults.next({ query: '', results: [
setSearchResults('', [
{ path: 'tutorial', title: 'Tutorial index', type: '', keywords: '', titleWords: '' },
{ path: 'tutorial/toh-pt1', title: 'Tutorial - part 1', type: '', keywords: '', titleWords: '' },
]});
]);
expect(component.searchAreas).toEqual([
{ name: 'tutorial', priorityPages: [
{ path: 'tutorial', title: 'Tutorial index', type: '', keywords: '', titleWords: '' },
@ -85,21 +80,21 @@ describe('SearchResultsComponent', () => {
it('should put first 5 results for each area into priorityPages', () => {
const results = getTestResults();
searchResults.next({ query: '', results: results });
setSearchResults('', results);
expect(component.searchAreas[0].priorityPages).toEqual(results.filter(p => p.path.startsWith('api')).slice(0, 5));
expect(component.searchAreas[1].priorityPages).toEqual(results.filter(p => p.path.startsWith('guide')).slice(0, 5));
});
it('should put the nonPriorityPages into the pages array, sorted by title', () => {
const results = getTestResults();
searchResults.next({ query: '', results: results });
setSearchResults('', results);
expect(component.searchAreas[0].pages).toEqual([]);
expect(component.searchAreas[1].pages).toEqual(results.filter(p => p.path.startsWith('guide')).slice(5).sort(compareTitle));
});
it('should put a total count in the header of each area of search results', () => {
const results = getTestResults();
searchResults.next({ query: '', results: results });
setSearchResults('', results);
fixture.detectChanges();
const headers = fixture.debugElement.queryAll(By.css('h3'));
expect(headers.length).toEqual(2);
@ -112,7 +107,7 @@ describe('SearchResultsComponent', () => {
{ path: 'news', title: 'News', type: 'marketing', keywords: '', titleWords: '' }
];
searchResults.next({ query: '', results: results });
setSearchResults('', results);
expect(component.searchAreas).toEqual([
{ name: 'other', priorityPages: [
{ path: 'news', title: 'News', type: 'marketing', keywords: '', titleWords: '' }
@ -125,7 +120,7 @@ describe('SearchResultsComponent', () => {
{ path: 'news', title: undefined, type: 'marketing', keywords: '', titleWords: '' }
];
searchResults.next({ query: 'something', results: results });
setSearchResults('something', results);
expect(component.searchAreas).toEqual([]);
});
@ -144,7 +139,7 @@ describe('SearchResultsComponent', () => {
selected = null;
searchResult = { path: 'news', title: 'News', type: 'marketing', keywords: '', titleWords: '' };
searchResults.next({ query: 'something', results: [searchResult] });
setSearchResults('something', [searchResult]);
fixture.detectChanges();
anchor = fixture.debugElement.query(By.css('a'));
@ -179,10 +174,8 @@ describe('SearchResultsComponent', () => {
describe('when no query results', () => {
it('should display "not found" message', () => {
searchResults.next({ query: 'something', results: [] });
fixture.detectChanges();
setSearchResults('something', []);
expect(getText()).toContain('No results');
});
});
});

View File

@ -1,28 +1,20 @@
import { Component, EventEmitter, HostListener, OnDestroy, OnInit, Output } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription';
import { SearchResult, SearchResults, SearchService } from '../search.service';
export interface SearchArea {
name: string;
pages: SearchResult[];
priorityPages: SearchResult[];
}
import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
import { SearchResult, SearchResults, SearchArea } from 'app/search/interfaces';
/**
* A component to display the search results
* A component to display search results in groups
*/
@Component({
selector: 'aio-search-results',
templateUrl: './search-results.component.html',
})
export class SearchResultsComponent implements OnInit, OnDestroy {
export class SearchResultsComponent implements OnChanges {
private resultsSubscription: Subscription;
readonly defaultArea = 'other';
notFoundMessage = 'Searching ...';
readonly topLevelFolders = ['guide', 'tutorial'];
/**
* The results to display
*/
@Input()
searchResults: SearchResults;
/**
* Emitted when the user selects a search result
@ -30,20 +22,13 @@ export class SearchResultsComponent implements OnInit, OnDestroy {
@Output()
resultSelected = new EventEmitter<SearchResult>();
/**
* A mapping of the search results grouped into areas
*/
readonly defaultArea = 'other';
notFoundMessage = 'Searching ...';
readonly topLevelFolders = ['guide', 'tutorial'];
searchAreas: SearchArea[] = [];
constructor(private searchService: SearchService) {}
ngOnInit() {
this.resultsSubscription = this.searchService.searchResults
.subscribe(search => this.searchAreas = this.processSearchResults(search));
}
ngOnDestroy() {
this.resultsSubscription.unsubscribe();
ngOnChanges(changes: SimpleChanges) {
this.searchAreas = this.processSearchResults(this.searchResults);
}
onResultSelected(page: SearchResult, event: MouseEvent) {
@ -55,6 +40,9 @@ export class SearchResultsComponent implements OnInit, OnDestroy {
// Map the search results into groups by area
private processSearchResults(search: SearchResults) {
if (!search) {
return [];
}
this.notFoundMessage = 'No results found.';
const searchAreaMap = {};
search.results.forEach(result => {
@ -84,6 +72,6 @@ export class SearchResultsComponent implements OnInit, OnDestroy {
}
}
function compareResults(l: {title: string}, r: {title: string}) {
function compareResults(l: SearchResult, r: SearchResult) {
return l.title.toUpperCase() > r.title.toUpperCase() ? 1 : -1;
}

View File

@ -1,5 +1,5 @@
import { Component, DebugElement } from '@angular/core';
import { async, ComponentFixture, TestBed, inject } from '@angular/core/testing';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { SelectComponent, Option } from './select.component';

View File

@ -1,5 +1,6 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { SearchResultsComponent } from './search-results/search-results.component';
import { SelectComponent } from './select/select.component';
@NgModule({
@ -7,9 +8,11 @@ import { SelectComponent } from './select/select.component';
CommonModule
],
exports: [
SearchResultsComponent,
SelectComponent
],
declarations: [
SearchResultsComponent,
SelectComponent
]
})

View File

@ -1,4 +1,4 @@
import { ReflectiveInjector, SecurityContext } from '@angular/core';
import { ReflectiveInjector } from '@angular/core';
import { DOCUMENT, DomSanitizer, SafeHtml } from '@angular/platform-browser';
import { Subject } from 'rxjs/Subject';
@ -268,7 +268,6 @@ describe('TocService', () => {
});
it('should calculate and set id of heading without an id', () => {
const tocItem = lastTocList.find(item => item.title === 'H2 Two');
const id = headings[2].getAttribute('id');
expect(id).toEqual('h2-two');
});

View File

@ -4,7 +4,7 @@ Use of this source code is governed by an MIT-style license that
can be found in the LICENSE file at http://angular.io/license
*/
import {NgZone, Injectable} from '@angular/core';
import {NgZone} from '@angular/core';
import {Observable} from 'rxjs/Observable';
export interface WebWorkerMessage {

View File

@ -0,0 +1,4 @@
import { InjectionToken } from '@angular/core';
export const WindowToken = new InjectionToken('Window');
export function windowProvider() { return window; }

View File

@ -1,4 +1,4 @@
import { Inject, Injectable, OnDestroy } from '@angular/core';
import { Injectable, OnDestroy } from '@angular/core';
import { NgServiceWorker } from '@angular/service-worker';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';

View File

@ -34,9 +34,13 @@
<!-- Google Analytics -->
<script>
window.ga=window.ga||function(){(ga.q=ga.q||[]).push(arguments)};ga.l=+new Date;
// Note this is a customised version of the GA tracking snippet to aid e2e testing
// See the bit between /**/.../**/
(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;/**/i.sessionStorage.__e2e__||/**/m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
</script>
<script async src='https://www.google-analytics.com/analytics.js'></script>
<!-- End Google Analytics -->
<script>

View File

@ -29,6 +29,24 @@ aio-search-results {
}
}
aio-search-results.embedded .search-results {
padding: 0;
color: inherit;
width: auto;
max-height: 100%;
position: relative;
background-color: inherit;
box-shadow: none;
box-sizing: border-box;
.search-area a {
color: lighten($darkgray, 10);
&:hover {
color: $accentblue;
}
}
}
.search-area {
display: flex;
flex-direction: column;

View File

@ -1,5 +1,5 @@
import { Subject } from 'rxjs/Subject';
import { SearchResults } from 'app/search/search.service';
import { SearchResults } from 'app/search/interfaces';
export class MockSearchService {
searchResults = new Subject<SearchResults>();

View File

@ -68,7 +68,7 @@
// other libraries
'rxjs': 'npm:rxjs@5.0.1',
'tslib': 'npm:tslib/tslib.js',
'angular-in-memory-web-api': 'npm:angular-in-memory-web-api/bundles/in-memory-web-api.umd.js',
'angular-in-memory-web-api': 'npm:angular-in-memory-web-api@0.4/bundles/in-memory-web-api.umd.js',
'ts': 'npm:plugin-typescript@5.2.7/lib/plugin.js',
'typescript': 'npm:typescript@2.3.2/lib/typescript.js',

View File

@ -54,7 +54,7 @@
// other libraries
'rxjs': 'npm:rxjs@5.0.1',
'tslib': 'npm:tslib/tslib.js',
'angular-in-memory-web-api': 'npm:angular-in-memory-web-api/bundles/in-memory-web-api.umd.js',
'angular-in-memory-web-api': 'npm:angular-in-memory-web-api@0.4/bundles/in-memory-web-api.umd.js',
'ts': 'npm:plugin-typescript@5.2.7/lib/plugin.js',
'typescript': 'npm:typescript@2.3.2/lib/typescript.js',

View File

@ -20,7 +20,11 @@ exports.config = {
// Capabilities to be passed to the webdriver instance.
capabilities: {
'browserName': 'chrome'
'browserName': 'chrome',
// For Travis
chromeOptions: {
binary: process.env.CHROME_BIN
}
},
// Framework to use. Jasmine is recommended.

View File

@ -1027,7 +1027,7 @@ chalk@^1.1.1, chalk@^1.1.3:
strip-ansi "^3.0.0"
supports-color "^2.0.0"
chalk@^2.0.0:
chalk@^2.0.0, chalk@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.1.0.tgz#ac5becf14fa21b99c6c92ca7a7d7cfd5b17e743e"
dependencies:
@ -2770,6 +2770,10 @@ hoek@2.x.x:
version "2.16.3"
resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed"
hoek@4.x.x:
version "4.2.0"
resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d"
homedir-polyfill@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/homedir-polyfill/-/homedir-polyfill-1.0.1.tgz#4c2bbc8a758998feebf5ed68580f76d46768b4bc"

View File

@ -7,7 +7,6 @@ const shelljs = require('shelljs');
const yargs = require('yargs');
const PACKAGE_JSON = 'package.json';
const LOCKFILE = 'yarn.lock';
const LOCAL_MARKER_PATH = 'node_modules/_local_.json';
const PACKAGE_JSON_REGEX = /^[^/]+\/package\.json$/;
@ -66,27 +65,61 @@ class NgPackagesInstaller {
if (this._checkLocalMarker() !== true || this.force) {
const pathToPackageConfig = path.resolve(this.projectDir, PACKAGE_JSON);
const packages = this._getDistPackages();
const packageConfigFile = fs.readFileSync(pathToPackageConfig);
const packageConfig = JSON.parse(packageConfigFile);
const [dependencies, peers] = this._collectDependencies(packageConfig.dependencies || {}, packages);
const [devDependencies, devPeers] = this._collectDependencies(packageConfig.devDependencies || {}, packages);
this._assignPeerDependencies(peers, dependencies, devDependencies);
this._assignPeerDependencies(devPeers, dependencies, devDependencies);
const localPackageConfig = Object.assign(Object.create(null), packageConfig, { dependencies, devDependencies });
localPackageConfig.__angular = { local: true };
const localPackageConfigJson = JSON.stringify(localPackageConfig, null, 2);
try {
this._log(`Writing temporary local ${PACKAGE_JSON} to ${pathToPackageConfig}`);
fs.writeFileSync(pathToPackageConfig, localPackageConfigJson);
this._installDeps('--no-lockfile', '--check-files');
this._setLocalMarker(localPackageConfigJson);
// Overwrite local Angular packages dependencies to other Angular packages with local files.
Object.keys(packages).forEach(key => {
const pkg = packages[key];
const tmpConfig = JSON.parse(JSON.stringify(pkg.config));
// Prevent accidental publishing of the package, if something goes wrong.
tmpConfig.private = true;
// Overwrite project dependencies/devDependencies to Angular packages with local files.
['dependencies', 'devDependencies'].forEach(prop => {
const deps = tmpConfig[prop] || {};
Object.keys(deps).forEach(key2 => {
const pkg2 = packages[key2];
if (pkg2) {
// point the core Angular packages at the distributable folder
deps[key2] = `file:${pkg2.parentDir}/${key2.replace('@angular/', '')}`;
this._log(`Overriding dependency of local ${key} with local package: ${key2}: ${deps[key2]}`);
}
});
});
fs.writeFileSync(pkg.packageJsonPath, JSON.stringify(tmpConfig));
});
const packageConfigFile = fs.readFileSync(pathToPackageConfig);
const packageConfig = JSON.parse(packageConfigFile);
const [dependencies, peers] = this._collectDependencies(packageConfig.dependencies || {}, packages);
const [devDependencies, devPeers] = this._collectDependencies(packageConfig.devDependencies || {}, packages);
this._assignPeerDependencies(peers, dependencies, devDependencies);
this._assignPeerDependencies(devPeers, dependencies, devDependencies);
const localPackageConfig = Object.assign(Object.create(null), packageConfig, { dependencies, devDependencies });
localPackageConfig.__angular = { local: true };
const localPackageConfigJson = JSON.stringify(localPackageConfig, null, 2);
try {
this._log(`Writing temporary local ${PACKAGE_JSON} to ${pathToPackageConfig}`);
fs.writeFileSync(pathToPackageConfig, localPackageConfigJson);
this._installDeps('--no-lockfile', '--check-files');
this._setLocalMarker(localPackageConfigJson);
} finally {
this._log(`Restoring original ${PACKAGE_JSON} to ${pathToPackageConfig}`);
fs.writeFileSync(pathToPackageConfig, packageConfigFile);
}
} finally {
this._log(`Restoring original ${PACKAGE_JSON} to ${pathToPackageConfig}`);
fs.writeFileSync(pathToPackageConfig, packageConfigFile);
// Restore local Angular packages dependencies to other Angular packages.
this._log(`Restoring original ${PACKAGE_JSON} for local Angular packages.`);
Object.keys(packages).forEach(key => {
const pkg = packages[key];
fs.writeFileSync(pkg.packageJsonPath, JSON.stringify(pkg.config));
});
}
}
}
@ -96,7 +129,7 @@ class NgPackagesInstaller {
* Yarn will also delete the local marker file for us.
*/
restoreNpmDependencies() {
this._installDeps('--freeze-lockfile', '--check-files');
this._installDeps('--frozen-lockfile', '--check-files');
}
// Protected helpers
@ -122,13 +155,14 @@ class NgPackagesInstaller {
const sourcePackage = packages[key];
if (sourcePackage) {
// point the core Angular packages at the distributable folder
mergedDependencies[key] = `file:${ANGULAR_DIST_PACKAGES}/${key.replace('@angular/', '')}`;
mergedDependencies[key] = `file:${sourcePackage.parentDir}/${key.replace('@angular/', '')}`;
this._log(`Overriding dependency with local package: ${key}: ${mergedDependencies[key]}`);
// grab peer dependencies
Object.keys(sourcePackage.peerDependencies || {})
const sourcePackagePeerDeps = sourcePackage.config.peerDependencies || {};
Object.keys(sourcePackagePeerDeps)
// ignore peerDependencies which are already core Angular packages
.filter(key => !packages[key])
.forEach(key => peerDependencies[key] = sourcePackage.peerDependencies[key]);
.forEach(key => peerDependencies[key] = sourcePackagePeerDeps[key]);
}
});
return [mergedDependencies, peerDependencies];
@ -140,20 +174,29 @@ class NgPackagesInstaller {
*/
_getDistPackages() {
const packageConfigs = Object.create(null);
this._log(`Angular distributable directory: ${ANGULAR_DIST_PACKAGES}.`);
shelljs
.find(ANGULAR_DIST_PACKAGES)
.map(filePath => filePath.slice(ANGULAR_DIST_PACKAGES.length + 1))
.filter(filePath => PACKAGE_JSON_REGEX.test(filePath))
.forEach(packagePath => {
const packageName = `@angular/${packagePath.slice(0, -PACKAGE_JSON.length -1)}`;
if (this.ignorePackages.indexOf(packageName) === -1) {
const packageConfig = require(path.resolve(ANGULAR_DIST_PACKAGES, packagePath));
packageConfigs[packageName] = packageConfig;
} else {
this._log('Ignoring package', packageName);
}
});
[ANGULAR_DIST_PACKAGES].forEach(distDir => {
this._log(`Angular distributable directory: ${distDir}.`);
shelljs
.find(distDir)
.map(filePath => filePath.slice(distDir.length + 1))
.filter(filePath => PACKAGE_JSON_REGEX.test(filePath))
.forEach(packagePath => {
const packageName = `@angular/${packagePath.slice(0, -PACKAGE_JSON.length -1)}`;
if (this.ignorePackages.indexOf(packageName) === -1) {
const packageConfig = require(path.resolve(distDir, packagePath));
packageConfigs[packageName] = {
parentDir: distDir,
packageJsonPath: path.resolve(distDir, packagePath),
config: packageConfig
};
} else {
this._log('Ignoring package', packageName);
}
});
});
this._log('Found the following Angular distributables:', Object.keys(packageConfigs).map(key => `\n - ${key}`));
return packageConfigs;
}
@ -244,4 +287,4 @@ function main() {
module.exports = NgPackagesInstaller;
if (require.main === module) {
main();
}
}

View File

@ -12,6 +12,7 @@ describe('NgPackagesInstaller', () => {
const nodeModulesDir = path.resolve(absoluteRootDir, 'node_modules');
const packageJsonPath = path.resolve(absoluteRootDir, 'package.json');
const packagesDir = path.resolve(path.resolve(__dirname, '../../../dist/packages-dist'));
const toolsDir = path.resolve(path.resolve(__dirname, '../../../dist/tools/@angular'));
let installer;
beforeEach(() => {
@ -46,6 +47,7 @@ describe('NgPackagesInstaller', () => {
});
describe('installLocalDependencies()', () => {
const copyJsonObj = obj => JSON.parse(JSON.stringify(obj));
let dummyNgPackages, dummyPackage, dummyPackageJson, expectedModifiedPackage, expectedModifiedPackageJson;
beforeEach(() => {
@ -53,12 +55,39 @@ describe('NgPackagesInstaller', () => {
// These are the packages that are "found" in the dist directory
dummyNgPackages = {
'@angular/core': { peerDependencies: { rxjs: '5.0.1' } },
'@angular/common': { peerDependencies: { '@angular/core': '4.4.1' } },
'@angular/compiler': { },
'@angular/compiler-cli': { peerDependencies: { typescript: '^2.4.2', '@angular/compiler': '4.3.2' } }
'@angular/core': {
parentDir: packagesDir,
packageJsonPath: `${packagesDir}/core/package.json`,
config: { peerDependencies: { rxjs: '5.0.1' } }
},
'@angular/common': {
parentDir: packagesDir,
packageJsonPath: `${packagesDir}/common/package.json`,
config: { peerDependencies: { '@angular/core': '4.4.4-1ab23cd4' } }
},
'@angular/compiler': {
parentDir: packagesDir,
packageJsonPath: `${packagesDir}/compiler/package.json`,
config: { peerDependencies: { '@angular/common': '4.4.4-1ab23cd4' } }
},
'@angular/compiler-cli': {
parentDir: toolsDir,
packageJsonPath: `${toolsDir}/compiler-cli/package.json`,
config: {
dependencies: { '@angular/tsc-wrapped': '4.4.4-1ab23cd4' },
peerDependencies: { typescript: '^2.4.2', '@angular/compiler': '4.4.4-1ab23cd4' }
}
},
'@angular/tsc-wrapped': {
parentDir: toolsDir,
packageJsonPath: `${toolsDir}/tsc-wrapped/package.json`,
config: {
devDependencies: { '@angular/common': '4.4.4-1ab23cd4' },
peerDependencies: { tsickle: '^1.4.0' }
}
}
};
spyOn(installer, '_getDistPackages').and.returnValue(dummyNgPackages);
spyOn(installer, '_getDistPackages').and.callFake(() => copyJsonObj(dummyNgPackages));
// This is the package.json in the "test" folder
dummyPackage = {
@ -82,7 +111,7 @@ describe('NgPackagesInstaller', () => {
'@angular/common': `file:${packagesDir}/common`
},
devDependencies: {
'@angular/compiler-cli': `file:${packagesDir}/compiler-cli`,
'@angular/compiler-cli': `file:${toolsDir}/compiler-cli`,
rxjs: '5.0.1',
typescript: '^2.4.2'
},
@ -117,6 +146,33 @@ describe('NgPackagesInstaller', () => {
expect(installer._getDistPackages).toHaveBeenCalled();
});
it('should temporarily overwrite the package.json files of local Angular packages', () => {
const pkgJsonFor = pkgName => dummyNgPackages[`@angular/${pkgName}`].packageJsonPath;
const pkgConfigFor = pkgName => copyJsonObj(dummyNgPackages[`@angular/${pkgName}`].config);
const overwriteConfigFor = (pkgName, newProps) => Object.assign(pkgConfigFor(pkgName), newProps);
const allArgs = fs.writeFileSync.calls.allArgs();
const firstFiveArgs = allArgs.slice(0, 5);
const lastFiveArgs = allArgs.slice(-5);
expect(firstFiveArgs).toEqual([
[pkgJsonFor('core'), JSON.stringify(overwriteConfigFor('core', {private: true}))],
[pkgJsonFor('common'), JSON.stringify(overwriteConfigFor('common', {private: true}))],
[pkgJsonFor('compiler'), JSON.stringify(overwriteConfigFor('compiler', {private: true}))],
[pkgJsonFor('compiler-cli'), JSON.stringify(overwriteConfigFor('compiler-cli', {
private: true,
dependencies: { '@angular/tsc-wrapped': `file:${toolsDir}/tsc-wrapped` }
}))],
[pkgJsonFor('tsc-wrapped'), JSON.stringify(overwriteConfigFor('tsc-wrapped', {
private: true,
devDependencies: { '@angular/common': `file:${packagesDir}/common` }
}))],
]);
expect(lastFiveArgs).toEqual(['core', 'common', 'compiler', 'compiler-cli', 'tsc-wrapped']
.map(pkgName => [pkgJsonFor(pkgName), JSON.stringify(pkgConfigFor(pkgName))]));
});
it('should load the package.json', () => {
expect(fs.readFileSync).toHaveBeenCalledWith(packageJsonPath);
});
@ -147,29 +203,42 @@ describe('NgPackagesInstaller', () => {
it('should run `yarn install` in the specified directory, with the correct options', () => {
spyOn(installer, '_installDeps');
installer.restoreNpmDependencies();
expect(installer._installDeps).toHaveBeenCalledWith('--freeze-lockfile', '--check-files');
expect(installer._installDeps).toHaveBeenCalledWith('--frozen-lockfile', '--check-files');
});
});
describe('_getDistPackages', () => {
describe('_getDistPackages()', () => {
it('should include top level Angular packages', () => {
const ngPackages = installer._getDistPackages();
const expectedValue = jasmine.objectContaining({
parentDir: jasmine.any(String),
packageJsonPath: jasmine.any(String),
config: jasmine.any(Object),
});
// For example...
expect(ngPackages['@angular/common']).toEqual(expectedValue);
expect(ngPackages['@angular/core']).toEqual(expectedValue);
expect(ngPackages['@angular/router']).toEqual(expectedValue);
expect(ngPackages['@angular/upgrade']).toEqual(expectedValue);
expect(ngPackages['@angular/upgrade/static']).not.toBeDefined();
});
it('should store each package\'s parent directory', () => {
const ngPackages = installer._getDistPackages();
// For example...
expect(ngPackages['@angular/core'].parentDir).toBe(packagesDir);
expect(ngPackages['@angular/router'].parentDir).toBeDefined(toolsDir);
});
it('should not include packages that have been ignored', () => {
installer = new NgPackagesInstaller(rootDir, { ignorePackages: ['@angular/router'] });
const ngPackages = installer._getDistPackages();
expect(ngPackages['@angular/common']).toBeDefined();
expect(ngPackages['@angular/core']).toBeDefined();
expect(ngPackages['@angular/router']).toBeDefined();
expect(ngPackages['@angular/upgrade']).toBeDefined();
expect(ngPackages['@angular/upgrade/static']).not.toBeDefined();
it('should not include packages that have been ignored', () => {
installer = new NgPackagesInstaller(rootDir, { ignorePackages: ['@angular/router'] });
const ngPackages = installer._getDistPackages();
expect(ngPackages['@angular/common']).toBeDefined();
expect(ngPackages['@angular/router']).toBeUndefined();
});
expect(ngPackages['@angular/router']).toBeUndefined();
});
});
@ -195,7 +264,7 @@ describe('NgPackagesInstaller', () => {
});
});
describe('_printWarning', () => {
describe('_printWarning()', () => {
it('should mention the message passed in the warning', () => {
installer._printWarning();
expect(console.warn.calls.argsFor(0)[0]).toContain('is running against the local Angular build');
@ -218,7 +287,7 @@ describe('NgPackagesInstaller', () => {
});
});
describe('_installDeps', () => {
describe('_installDeps()', () => {
it('should run yarn install with the given options', () => {
installer._installDeps('option-1', 'option-2');
expect(shelljs.exec).toHaveBeenCalledWith('yarn install option-1 option-2', { cwd: absoluteRootDir });

View File

@ -63,6 +63,7 @@ module.exports = new Package('angular-api', [basePackage, typeScriptPackage])
'router/index.ts',
'router/testing/index.ts',
'router/upgrade/index.ts',
'service-worker/index.ts',
'upgrade/index.ts',
'upgrade/static/index.ts',
];

View File

@ -21,6 +21,7 @@ const packageMap = {
'platform-webworker': ['platform-webworker/index.ts'],
'platform-webworker-dynamic': 'platform-webworker-dynamic/index.ts',
router: ['router/index.ts', 'router/testing/index.ts', 'router/upgrade/index.ts'],
'service-worker': ['service-worker/index.ts'],
upgrade: ['upgrade/index.ts', 'upgrade/static/index.ts']
};

View File

@ -8,6 +8,7 @@
"moduleResolution": "node",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"noUnusedLocals": true,
"target": "es5",
"typeRoots": [
"node_modules/@types"

View File

@ -6564,13 +6564,7 @@ rx@2.3.24:
version "2.3.24"
resolved "https://registry.yarnpkg.com/rx/-/rx-2.3.24.tgz#14f950a4217d7e35daa71bbcbe58eff68ea4b2b7"
rxjs@^5.2.0:
version "5.4.0"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.4.0.tgz#a7db14ab157f9d7aac6a56e655e7a3860d39bf26"
dependencies:
symbol-observable "^1.0.1"
rxjs@^5.4.2:
rxjs@^5.2.0, rxjs@^5.4.2:
version "5.4.3"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.4.3.tgz#0758cddee6033d68e0fd53676f0f3596ce3d483f"
dependencies:

View File

@ -1,6 +1,6 @@
{
"name": "angular-srcs",
"version": "5.0.0-rc.2",
"version": "5.0.0-rc.7",
"private": true,
"branchPattern": "2.0.*",
"description": "Angular - a web framework for modern web apps",
@ -24,7 +24,7 @@
"dependencies": {
"core-js": "^2.4.1",
"reflect-metadata": "^0.1.3",
"rxjs": "5.x",
"rxjs": "^5.5.2",
"tslib": "^1.7.1",
"zone.js": "^0.8.12"
},
@ -90,6 +90,7 @@
"rollup-plugin-sourcemaps": "0.4.2",
"selenium-webdriver": "3.5.0",
"semver": "5.4.1",
"shelljs": "^0.7.8",
"source-map": "0.5.7",
"source-map-support": "0.4.18",
"systemjs": "0.18.10",

View File

@ -68,7 +68,7 @@ export class StateValue {
get params(): {[key: string]: any} { return this.options.params as{[key: string]: any}; }
constructor(input: any) {
constructor(input: any, public namespaceId: string = '') {
const isObj = input && input.hasOwnProperty('value');
const value = isObj ? input['value'] : input;
this.value = normalizeTriggerValue(value);
@ -141,7 +141,7 @@ export class AnimationTransitionNamespace {
if (!triggersWithStates.hasOwnProperty(name)) {
addClass(element, NG_TRIGGER_CLASSNAME);
addClass(element, NG_TRIGGER_CLASSNAME + '-' + name);
triggersWithStates[name] = null;
triggersWithStates[name] = DEFAULT_STATE_VALUE;
}
return () => {
@ -192,7 +192,7 @@ export class AnimationTransitionNamespace {
}
let fromState = triggersWithStates[triggerName];
const toState = new StateValue(value);
const toState = new StateValue(value, this.id);
const isObj = value && value.hasOwnProperty('value');
if (!isObj && fromState) {
@ -305,38 +305,31 @@ export class AnimationTransitionNamespace {
}
}
private _destroyInnerNodes(rootElement: any, context: any, animate: boolean = false) {
private _signalRemovalForInnerTriggers(rootElement: any, context: any, animate: boolean = false) {
// emulate a leave animation for all inner nodes within this node.
// If there are no animations found for any of the nodes then clear the cache
// for the element.
this._engine.driver.query(rootElement, NG_TRIGGER_SELECTOR, true).forEach(elm => {
if (animate && containsClass(elm, this._hostClassName)) {
const innerNs = this._engine.namespacesByHostElement.get(elm);
// special case for a host element with animations on the same element
if (innerNs) {
innerNs.removeNode(elm, context, true);
}
this.removeNode(elm, context, true);
const namespaces = this._engine.fetchNamespacesByElement(elm);
if (namespaces.size) {
namespaces.forEach(ns => { ns.triggerLeaveAnimation(elm, context, false, true); });
} else {
this.clearElementCache(elm);
}
});
}
removeNode(element: any, context: any, doNotRecurse?: boolean): void {
const engine = this._engine;
if (!doNotRecurse && element.childElementCount) {
this._destroyInnerNodes(element, context, true);
}
const triggerStates = engine.statesByElement.get(element);
triggerLeaveAnimation(
element: any, context: any, destroyAfterComplete?: boolean,
defaultToFallback?: boolean): boolean {
const triggerStates = this._engine.statesByElement.get(element);
if (triggerStates) {
const players: TransitionAnimationPlayer[] = [];
Object.keys(triggerStates).forEach(triggerName => {
// this check is here in the event that an element is removed
// twice (both on the host level and the component level)
if (this._triggers[triggerName]) {
const player = this.trigger(element, triggerName, VOID_VALUE, false);
const player = this.trigger(element, triggerName, VOID_VALUE, defaultToFallback);
if (player) {
players.push(player);
}
@ -344,11 +337,55 @@ export class AnimationTransitionNamespace {
});
if (players.length) {
engine.markElementAsRemoved(this.id, element, true, context);
optimizeGroupPlayer(players).onDone(() => engine.processLeaveNode(element));
return;
this._engine.markElementAsRemoved(this.id, element, true, context);
if (destroyAfterComplete) {
optimizeGroupPlayer(players).onDone(() => this._engine.processLeaveNode(element));
}
return true;
}
}
return false;
}
prepareLeaveAnimationListeners(element: any) {
const listeners = this._elementListeners.get(element);
if (listeners) {
const visitedTriggers = new Set<string>();
listeners.forEach(listener => {
const triggerName = listener.name;
if (visitedTriggers.has(triggerName)) return;
visitedTriggers.add(triggerName);
const trigger = this._triggers[triggerName];
const transition = trigger.fallbackTransition;
const elementStates = this._engine.statesByElement.get(element) !;
const fromState = elementStates[triggerName] || DEFAULT_STATE_VALUE;
const toState = new StateValue(VOID_VALUE);
const player = new TransitionAnimationPlayer(this.id, triggerName, element);
this._engine.totalQueuedPlayers++;
this._queue.push({
element,
triggerName,
transition,
fromState,
toState,
player,
isFallbackTransition: true
});
});
}
}
removeNode(element: any, context: any): void {
const engine = this._engine;
if (element.childElementCount) {
this._signalRemovalForInnerTriggers(element, context, true);
}
// this means that a * => VOID animation was detected and kicked off
if (this.triggerLeaveAnimation(element, context, true)) return;
// find the player that is animating and make sure that the
// removal is delayed until that player has completed
@ -379,33 +416,7 @@ export class AnimationTransitionNamespace {
// during flush or will be picked up by a parent query. Either way
// we need to fire the listeners for this element when it DOES get
// removed (once the query parent animation is done or after flush)
const listeners = this._elementListeners.get(element);
if (listeners) {
const visitedTriggers = new Set<string>();
listeners.forEach(listener => {
const triggerName = listener.name;
if (visitedTriggers.has(triggerName)) return;
visitedTriggers.add(triggerName);
const trigger = this._triggers[triggerName];
const transition = trigger.fallbackTransition;
const elementStates = engine.statesByElement.get(element) !;
const fromState = elementStates[triggerName] || DEFAULT_STATE_VALUE;
const toState = new StateValue(VOID_VALUE);
const player = new TransitionAnimationPlayer(this.id, triggerName, element);
this._engine.totalQueuedPlayers++;
this._queue.push({
element,
triggerName,
transition,
fromState,
toState,
player,
isFallbackTransition: true
});
});
}
this.prepareLeaveAnimationListeners(element);
// whether or not a parent has an animation we need to delay the deferral of the leave
// operation until we have more information (which we do after flush() has been called)
@ -468,7 +479,7 @@ export class AnimationTransitionNamespace {
destroy(context: any) {
this.players.forEach(p => p.destroy());
this._destroyInnerNodes(this.hostElement, context);
this._signalRemovalForInnerTriggers(this.hostElement, context);
}
elementContainsData(element: any): boolean {
@ -603,6 +614,29 @@ export class TransitionAnimationEngine {
private _fetchNamespace(id: string) { return this._namespaceLookup[id]; }
fetchNamespacesByElement(element: any): Set<AnimationTransitionNamespace> {
// normally there should only be one namespace per element, however
// if @triggers are placed on both the component element and then
// its host element (within the component code) then there will be
// two namespaces returned. We use a set here to simply the dedupe
// of namespaces incase there are multiple triggers both the elm and host
const namespaces = new Set<AnimationTransitionNamespace>();
const elementStates = this.statesByElement.get(element);
if (elementStates) {
const keys = Object.keys(elementStates);
for (let i = 0; i < keys.length; i++) {
const nsId = elementStates[keys[i]].namespaceId;
if (nsId) {
const ns = this._fetchNamespace(nsId);
if (ns) {
namespaces.add(ns);
}
}
}
}
return namespaces;
}
trigger(namespaceId: string, element: any, name: string, value: any): boolean {
if (isElementNode(element)) {
this._fetchNamespace(namespaceId).trigger(element, name, value);
@ -648,7 +682,7 @@ export class TransitionAnimationEngine {
}
}
removeNode(namespaceId: string, element: any, context: any, doNotRecurse?: boolean): void {
removeNode(namespaceId: string, element: any, context: any): void {
if (!isElementNode(element)) {
this._onRemovalComplete(element, context);
return;
@ -656,7 +690,7 @@ export class TransitionAnimationEngine {
const ns = namespaceId ? this._fetchNamespace(namespaceId) : null;
if (ns) {
ns.removeNode(element, context, doNotRecurse);
ns.removeNode(element, context);
} else {
this.markElementAsRemoved(namespaceId, element, false, context);
}
@ -688,37 +722,39 @@ export class TransitionAnimationEngine {
destroyInnerAnimations(containerElement: any) {
let elements = this.driver.query(containerElement, NG_TRIGGER_SELECTOR, true);
elements.forEach(element => {
const players = this.playersByElement.get(element);
if (players) {
players.forEach(player => {
// special case for when an element is set for destruction, but hasn't started.
// in this situation we want to delay the destruction until the flush occurs
// so that any event listeners attached to the player are triggered.
if (player.queued) {
player.markedForDestroy = true;
} else {
player.destroy();
}
});
}
const stateMap = this.statesByElement.get(element);
if (stateMap) {
Object.keys(stateMap).forEach(triggerName => stateMap[triggerName] = DELETED_STATE_VALUE);
}
});
elements.forEach(element => this.destroyActiveAnimationsForElement(element));
if (this.playersByQueriedElement.size == 0) return;
elements = this.driver.query(containerElement, NG_ANIMATING_SELECTOR, true);
if (elements.length) {
elements.forEach(element => {
const players = this.playersByQueriedElement.get(element);
if (players) {
players.forEach(player => player.finish());
elements.forEach(element => this.finishActiveQueriedAnimationOnElement(element));
}
destroyActiveAnimationsForElement(element: any) {
const players = this.playersByElement.get(element);
if (players) {
players.forEach(player => {
// special case for when an element is set for destruction, but hasn't started.
// in this situation we want to delay the destruction until the flush occurs
// so that any event listeners attached to the player are triggered.
if (player.queued) {
player.markedForDestroy = true;
} else {
player.destroy();
}
});
}
const stateMap = this.statesByElement.get(element);
if (stateMap) {
Object.keys(stateMap).forEach(triggerName => stateMap[triggerName] = DELETED_STATE_VALUE);
}
}
finishActiveQueriedAnimationOnElement(element: any) {
const players = this.playersByQueriedElement.get(element);
if (players) {
players.forEach(player => player.finish());
}
}
whenRenderingDone(): Promise<any> {

View File

@ -853,7 +853,7 @@ export function transition(
* var fadeAnimation = animation([
* style({ opacity: '{{ start }}' }),
* animate('{{ time }}',
* style({ opacity: '{{ end }}'))
* style({ opacity: '{{ end }}'}))
* ], { params: { time: '1000ms', start: 0, end: 1 }});
* ```
*

View File

@ -6,7 +6,7 @@
"license": "MIT",
"peerDependencies": {
"@angular/compiler-cli": "0.0.0-PLACEHOLDER",
"typescript": "^2.4.2"
"typescript": ">=2.4.2 <2.5"
},
"dependencies": {
"@bazel/typescript": "0.2.x",

View File

@ -50,7 +50,7 @@ def _expected_outs(ctx, label):
declaration_files += [ctx.new_file(ctx.bin_dir, basename + ext) for ext in declarations]
summary_files += [ctx.new_file(ctx.bin_dir, basename + ext) for ext in summaries]
i18n_messages_files = [ctx.new_file(ctx.bin_dir, ctx.label.name + "_ngc_messages.xmb")]
i18n_messages_files = [ctx.new_file(ctx.genfiles_dir, ctx.label.name + "_ngc_messages.xmb")]
return struct(
closure_js = closure_js_files,
@ -115,7 +115,7 @@ def ngc_compile_action(ctx, label, inputs, outputs, messages_out, config_file_pa
else:
supports_workers = str(int(ctx.attr._supports_workers))
arguments = _EXTRA_NODE_OPTIONS_FLAGS
arguments = list(_EXTRA_NODE_OPTIONS_FLAGS)
# One at-sign makes this a params-file, enabling the worker strategy.
# Two at-signs escapes the argument so it's passed through to ngc
# rather than the contents getting expanded.
@ -144,7 +144,10 @@ def ngc_compile_action(ctx, label, inputs, outputs, messages_out, config_file_pa
executable = ctx.executable._ng_xi18n,
arguments = (_EXTRA_NODE_OPTIONS_FLAGS +
[config_file_path] +
[messages_out[0].short_path]),
# The base path is bin_dir because of the way the ngc
# compiler host is configured. So we need to explictily
# point to genfiles/ to redirect the output.
["../genfiles/" + messages_out[0].short_path]),
progress_message = "Extracting Angular 2 messages (ng_xi18n)",
mnemonic = "Angular2MessageExtractor")
@ -247,4 +250,4 @@ ng_module = rule(
),
},
outputs = COMMON_OUTPUTS,
)
)

View File

@ -1,15 +1,13 @@
load("@build_bazel_rules_nodejs//:defs.bzl", "nodejs_binary")
load("@build_bazel_rules_typescript//:defs.bzl", "ts_library")
licenses(["notice"]) # Apache 2.0
ts_library(
name = "ngc_lib",
srcs = [
"index.ts",
"emit_cache.ts",
"extract_i18n.ts",
],
module_name = "@angular/bazel",
deps = [
# BEGIN-INTERNAL
# Only needed when compiling within the Angular repo.
@ -19,6 +17,7 @@ ts_library(
"@build_bazel_rules_typescript//internal/tsc_wrapped"
],
tsconfig = ":tsconfig.json",
visibility = ["//test/ngc-wrapped:__subpackages__"],
)
nodejs_binary(

View File

@ -1,117 +0,0 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as ng from '@angular/compiler-cli';
import {CompilerHost, debug, fixUmdModuleDeclarations} from '@bazel/typescript';
import * as tsickle from 'tsickle';
import * as ts from 'typescript';
interface EmitCacheEntry {
emitResult: tsickle.EmitResult;
writtenFiles: Array<{fileName: string, content: string, sourceFiles?: ts.SourceFile[]}>;
generatedFile: ng.GeneratedFile;
}
interface SourceFileWithEmitCache extends ts.SourceFile {
emitCache: Map<string, EmitCacheEntry>;
}
function getCache(sf: ts.SourceFile, genFileName?: string): EmitCacheEntry|undefined {
const emitCache = (sf as SourceFileWithEmitCache).emitCache;
return emitCache ? emitCache.get(genFileName || sf.fileName) : undefined;
}
function setCache(sf: ts.SourceFile, entry: EmitCacheEntry) {
let emitCache = (sf as SourceFileWithEmitCache).emitCache;
if (!emitCache) {
emitCache = new Map();
(sf as SourceFileWithEmitCache).emitCache = emitCache;
}
emitCache.set(entry.generatedFile ? entry.generatedFile.genFileName : sf.fileName, entry);
}
export function getCachedGeneratedFile(sf: ts.SourceFile, genFileName: string): ng.GeneratedFile|
undefined {
const cacheEntry = getCache(sf, genFileName);
return cacheEntry ? cacheEntry.generatedFile : undefined;
}
export function emitWithCache(
program: ng.Program, inputsChanged: boolean, targetFileNames: string[],
compilerOpts: ng.CompilerOptions, host: CompilerHost): tsickle.EmitResult {
const emitCallback: ng.EmitCallback = ({
targetSourceFiles,
writeFile,
cancellationToken,
emitOnlyDtsFiles,
customTransformers = {}
}) => {
if (!targetSourceFiles) {
// Note: we know that we always have targetSourceFiles
// as we called `ng.Program.emit` with `targetFileNames`.
throw new Error('Unexpected state: no targetSourceFiles!');
}
let cacheHits = 0;
const mergedEmitResult = tsickle.mergeEmitResults(targetSourceFiles.map(targetSourceFile => {
const targetGeneratedFile = program.getGeneratedFile(targetSourceFile.fileName);
const cacheSf = targetGeneratedFile ?
program.getTsProgram().getSourceFile(targetGeneratedFile.srcFileName) :
targetSourceFile;
const cacheEntry = getCache(cacheSf, targetGeneratedFile && targetGeneratedFile.genFileName);
if (cacheEntry) {
let useEmitCache = false;
if (targetGeneratedFile && !program.hasChanged(targetSourceFile.fileName)) {
// we emitted a GeneratedFile with the same content as before -> use the cache
useEmitCache = true;
} else if (!inputsChanged && !targetGeneratedFile) {
// this is an input and no inputs have changed -> use the cache
useEmitCache = true;
}
if (useEmitCache) {
cacheHits++;
cacheEntry.writtenFiles.forEach(
({fileName, content, sourceFiles}) => writeFile(
fileName, content, /*writeByteOrderMark*/ false, /*onError*/ undefined,
sourceFiles));
return cacheEntry.emitResult;
}
}
const writtenFiles:
Array<{fileName: string, content: string, sourceFiles?: ts.SourceFile[]}> = [];
const recordingWriteFile =
(fileName: string, content: string, writeByteOrderMark: boolean,
onError?: (message: string) => void, sourceFiles?: ts.SourceFile[]) => {
writtenFiles.push({fileName, content, sourceFiles});
writeFile(fileName, content, writeByteOrderMark, onError, sourceFiles);
};
const emitResult = tsickle.emitWithTsickle(
program.getTsProgram(), host, host, compilerOpts, targetSourceFile, recordingWriteFile,
cancellationToken, emitOnlyDtsFiles, {
beforeTs: customTransformers.before,
afterTs: [
...(customTransformers.after || []),
fixUmdModuleDeclarations((sf: ts.SourceFile) => host.amdModuleName(sf)),
],
});
setCache(cacheSf, {
emitResult,
writtenFiles,
generatedFile: targetGeneratedFile,
});
return emitResult;
}));
debug(`Emitted ${targetSourceFiles.length} files with ${cacheHits} cache hits`);
return mergedEmitResult;
};
return program
.emit({
targetFileNames,
emitCallback,
emitFlags: ng.EmitFlags.DTS | ng.EmitFlags.JS | ng.EmitFlags.Codegen
}) as tsickle.EmitResult;
}

View File

@ -11,6 +11,6 @@
// Entry point
if (require.main === module) {
const args = process.argv.slice(2);
console.error('>>> not yet implemented!');
console.error('>>> now yet implemented!');
process.exitCode = 1;
}

View File

@ -8,14 +8,12 @@
// TODO(tbosch): figure out why we need this as it breaks node code within ngc-wrapped
/// <reference types="node" />
import * as ng from '@angular/compiler-cli';
import {BazelOptions, CachedFileLoader, CompilerHost, FileCache, FileLoader, UncachedFileLoader, constructManifest, debug, parseTsconfig, runAsWorker, runWorkerLoop} from '@bazel/typescript';
import {BazelOptions, CachedFileLoader, CompilerHost, FileCache, FileLoader, UncachedFileLoader, constructManifest, debug, fixUmdModuleDeclarations, parseTsconfig, runAsWorker, runWorkerLoop} from '@bazel/typescript';
import * as fs from 'fs';
import * as path from 'path';
import * as tsickle from 'tsickle';
import * as ts from 'typescript';
import {emitWithCache, getCachedGeneratedFile} from './emit_cache';
const EXT = /(\.ts|\.d\.ts|\.js|\.jsx|\.tsx)$/;
const NGC_GEN_FILES = /^(.*?)\.(ngfactory|ngsummary|ngstyle|shim\.ngstyle)(.*)$/;
// FIXME: we should be able to add the assets to the tsconfig so FileLoader
@ -42,7 +40,7 @@ export function main(args) {
/** The one FileCache instance used in this process. */
const fileCache = new FileCache<ts.SourceFile>(debug);
function runOneBuild(args: string[], inputs?: {[path: string]: string}): boolean {
export function runOneBuild(args: string[], inputs?: {[path: string]: string}): boolean {
if (args[0] === '-p') args.shift();
// Strip leading at-signs, used to indicate a params file
const project = args[0].replace(/^@+/, '');
@ -62,6 +60,9 @@ function runOneBuild(args: string[], inputs?: {[path: string]: string}): boolean
inputs,
expectedOuts
});
if (diagnostics.length) {
console.error(ng.formatDiagnostics(diagnostics));
}
return diagnostics.every(d => d.category !== ts.DiagnosticCategory.Error);
}
@ -76,32 +77,23 @@ export function relativeToRootDirs(filePath: string, rootDirs: string[]): string
}
export function compile({allowNonHermeticReads, allDepsCompiledWithBazel = true, compilerOpts,
tsHost, bazelOpts, files, inputs, expectedOuts,
gatherDiagnostics = defaultGatherDiagnostics}: {
tsHost, bazelOpts, files, inputs, expectedOuts, gatherDiagnostics}: {
allowNonHermeticReads: boolean,
allDepsCompiledWithBazel?: boolean,
compilerOpts: ng.CompilerOptions,
tsHost: ts.CompilerHost, inputs?: {[path: string]: string},
bazelOpts: BazelOptions,
files: string[],
expectedOuts: string[],
gatherDiagnostics?: (program: ng.Program, inputsToCheck: ts.SourceFile[],
genFilesToCheck: ng.GeneratedFile[]) => ng.Diagnostics
expectedOuts: string[], gatherDiagnostics?: (program: ng.Program) => ng.Diagnostics
}): {diagnostics: ng.Diagnostics, program: ng.Program} {
let fileLoader: FileLoader;
const oldFiles = new Map<string, ts.SourceFile>();
if (inputs) {
fileLoader = new CachedFileLoader(fileCache, allowNonHermeticReads);
// Resolve the inputs to absolute paths to match TypeScript internals
const resolvedInputs: {[path: string]: string} = {};
for (const key of Object.keys(inputs)) {
const resolvedKey = path.resolve(key);
resolvedInputs[resolvedKey] = inputs[key];
const cachedSf = fileCache.getCache(resolvedKey);
if (cachedSf) {
oldFiles.set(resolvedKey, cachedSf);
}
resolvedInputs[path.resolve(key)] = inputs[key];
}
fileCache.updateCache(resolvedInputs);
} else {
@ -189,56 +181,38 @@ export function compile({allowNonHermeticReads, allDepsCompiledWithBazel = true,
path.resolve(bazelBin, fileName) + '.d.ts';
}
const oldProgram = {
getSourceFile: (fileName: string) => { return oldFiles.get(fileName); },
getGeneratedFile: (srcFileName: string, genFileName: string) => {
const sf = oldFiles.get(srcFileName);
return sf ? getCachedGeneratedFile(sf, genFileName) : undefined;
},
};
const program =
ng.createProgram({rootNames: files, host: ngHost, options: compilerOpts, oldProgram});
let inputsChanged = files.some(fileName => program.hasChanged(fileName));
const emitCallback: ng.TsEmitCallback = ({
program,
targetSourceFile,
writeFile,
cancellationToken,
emitOnlyDtsFiles,
customTransformers = {},
}) =>
tsickle.emitWithTsickle(
program, bazelHost, bazelHost, compilerOpts, targetSourceFile, writeFile,
cancellationToken, emitOnlyDtsFiles, {
beforeTs: customTransformers.before,
afterTs: [
...(customTransformers.after || []),
fixUmdModuleDeclarations((sf: ts.SourceFile) => bazelHost.amdModuleName(sf)),
],
});
let genFilesToCheck: ng.GeneratedFile[];
let inputsToCheck: ts.SourceFile[];
if (inputsChanged) {
// if an input file changed, we need to type check all
// of our compilation sources as well as all generated files.
inputsToCheck = bazelOpts.compilationTargetSrc.map(
fileName => program.getTsProgram().getSourceFile(fileName));
genFilesToCheck = program.getGeneratedFiles().filter(gf => gf.genFileName.endsWith('.ts'));
} else {
// if no input file changed, only type check the changed generated files
// as these don't influence each other nor the type check of the input files.
inputsToCheck = [];
genFilesToCheck = program.getGeneratedFiles().filter(
gf => program.hasChanged(gf.genFileName) && gf.genFileName.endsWith('.ts'));
}
debug(
`TypeChecking ${inputsToCheck ? inputsToCheck.length : 'all'} inputs and ${genFilesToCheck ? genFilesToCheck.length : 'all'} generated files`);
const diagnostics = [...gatherDiagnostics(program !, inputsToCheck, genFilesToCheck)];
let emitResult: tsickle.EmitResult|undefined;
if (!diagnostics.length) {
const targetFileNames = [...bazelOpts.compilationTargetSrc];
for (const genFile of program.getGeneratedFiles()) {
if (genFile.genFileName.endsWith('.ts')) {
targetFileNames.push(genFile.genFileName);
}
}
emitResult = emitWithCache(program, inputsChanged, targetFileNames, compilerOpts, bazelHost);
diagnostics.push(...emitResult.diagnostics);
if (!gatherDiagnostics) {
gatherDiagnostics = (program) =>
gatherDiagnosticsForInputsOnly(compilerOpts, bazelOpts, program);
}
const {diagnostics, emitResult, program} = ng.performCompilation(
{rootNames: files, options: compilerOpts, host: ngHost, emitCallback, gatherDiagnostics});
const tsickleEmitResult = emitResult as tsickle.EmitResult;
let externs = '/** @externs */\n';
if (diagnostics.length) {
console.error(ng.formatDiagnostics(compilerOpts, diagnostics));
} else if (emitResult) {
if (!diagnostics.length) {
if (bazelOpts.tsickleGenerateExterns) {
externs += tsickle.getGeneratedExterns(emitResult.externs);
externs += tsickle.getGeneratedExterns(tsickleEmitResult.externs);
}
if (bazelOpts.manifest) {
const manifest = constructManifest(emitResult.modulesManifest, bazelHost);
const manifest = constructManifest(tsickleEmitResult.modulesManifest, bazelHost);
fs.writeFileSync(bazelOpts.manifest, manifest);
}
}
@ -257,9 +231,14 @@ export function compile({allowNonHermeticReads, allDepsCompiledWithBazel = true,
return {program, diagnostics};
}
function defaultGatherDiagnostics(
ngProgram: ng.Program, inputsToCheck: ts.SourceFile[],
genFilesToCheck: ng.GeneratedFile[]): (ng.Diagnostic | ts.Diagnostic)[] {
function isCompilationTarget(bazelOpts: BazelOptions, sf: ts.SourceFile): boolean {
return !NGC_GEN_FILES.test(sf.fileName) &&
(bazelOpts.compilationTargetSrc.indexOf(sf.fileName) !== -1);
}
function gatherDiagnosticsForInputsOnly(
options: ng.CompilerOptions, bazelOpts: BazelOptions,
ngProgram: ng.Program): (ng.Diagnostic | ts.Diagnostic)[] {
const tsProgram = ngProgram.getTsProgram();
const diagnostics: (ng.Diagnostic | ts.Diagnostic)[] = [];
// These checks mirror ts.getPreEmitDiagnostics, with the important
@ -267,7 +246,7 @@ function defaultGatherDiagnostics(
// program.getDeclarationDiagnostics() it somehow corrupts the emit.
diagnostics.push(...tsProgram.getOptionsDiagnostics());
diagnostics.push(...tsProgram.getGlobalDiagnostics());
for (const sf of inputsToCheck) {
for (const sf of tsProgram.getSourceFiles().filter(f => isCompilationTarget(bazelOpts, f))) {
// Note: We only get the diagnostics for individual files
// to e.g. not check libraries.
diagnostics.push(...tsProgram.getSyntacticDiagnostics(sf));
@ -277,9 +256,7 @@ function defaultGatherDiagnostics(
// only gather the angular diagnostics if we have no diagnostics
// in any other files.
diagnostics.push(...ngProgram.getNgStructuralDiagnostics());
for (const genFile of genFilesToCheck) {
diagnostics.push(...ngProgram.getNgSemanticDiagnostics(genFile));
}
diagnostics.push(...ngProgram.getNgSemanticDiagnostics());
}
return diagnostics;
}

View File

@ -0,0 +1,39 @@
load("@build_bazel_rules_nodejs//:defs.bzl", "jasmine_node_test")
load("@build_bazel_rules_typescript//:defs.bzl", "ts_library")
ts_library(
name = "ngc_test_lib",
srcs = [
"index_test.ts",
"test_support.ts",
"tsconfig_template.ts",
],
deps = [
# BEGIN-INTERNAL
# Only needed when compiling within the Angular repo.
# Users will get this dependency from node_modules.
"@//packages/compiler-cli",
# END-INTERNAL
"//src/ngc-wrapped:ngc_lib"
],
tsconfig = ":tsconfig.json",
)
# We need a filegroup so that we can refer
# .d.ts files (by default, jasmine_node_test would get the .js files).
filegroup(
name = "angular_core",
srcs = ["@//packages/core"]
)
jasmine_node_test(
name = "ngc_test",
srcs = [":ngc_test_lib"],
data = [
"@build_bazel_rules_typescript//internal:worker_protocol.proto",
":angular_core",
"//test/ngc-wrapped/empty:empty_tsconfig.json",
"//test/ngc-wrapped/empty:tsconfig.json",
],
size="small",
)

View File

@ -0,0 +1,10 @@
load("@angular//:index.bzl", "ng_module")
package(default_visibility=["//test:__subpackages__"])
ng_module(
name = "empty",
srcs = ["empty.ts"],
deps = ["@//packages/core"],
tsconfig = ":tsconfig.json",
)

View File

@ -0,0 +1 @@
# Empty ng_module to capture the default tsconfig.json

View File

@ -0,0 +1,10 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
// Empty file for an empty ng_module
// to capture the tsconfig used by ng_module.

View File

@ -0,0 +1,7 @@
{
"compilerOptions": {
"paths": {
"@angular/core": ["../../../../dist/packages/core"]
}
}
}

View File

@ -0,0 +1,36 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as fs from 'fs';
import * as path from 'path';
import {setup} from './test_support';
describe('ngc_wrapped', () => {
it('should work', () => {
const {read, write, runOneBuild, writeConfig, shouldExist, basePath} = setup();
write('some_project/index.ts', `
import {Component} from '@angular/core';
console.log('works: ', Component);
`);
writeConfig({
srcTargetPath: 'some_project',
});
// expect no error
expect(runOneBuild()).toBe(true);
shouldExist('bazel-bin/some_project/index.js');
expect(read('bazel-bin/some_project/index.js'))
.toContain(`console.log('works: ', core_1.Component);`);
});
});

View File

@ -0,0 +1,161 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {runOneBuild} from '@angular/bazel';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import * as ts from 'typescript';
import {createTsConfig} from './tsconfig_template';
export interface TestSupport {
basePath: string;
runfilesPath: string;
angularCorePath: string;
writeConfig({
srcTargetPath, depPaths, pathMapping,
}: {
srcTargetPath: string,
depPaths?: string[],
pathMapping?: Array<{moduleName: string; path: string;}>,
}): void;
read(fileName: string): string;
write(fileName: string, content: string): void;
writeFiles(...mockDirs: {[fileName: string]: string}[]): void;
shouldExist(fileName: string): void;
shouldNotExist(fileName: string): void;
runOneBuild(): void;
}
export function setup(
{
bazelBin = 'bazel-bin', tsconfig = 'tsconfig.json',
}: {
bazelBin?: string,
tsconfig?: string,
} = {}): TestSupport {
const runfilesPath = process.env['RUNFILES'];
const basePath = makeTempDir(runfilesPath);
const bazelBinPath = path.resolve(basePath, bazelBin);
fs.mkdirSync(bazelBinPath);
const angularCorePath = path.resolve(runfilesPath, 'angular_src', 'packages', 'core');
const ngFiles = listFilesRecursive(angularCorePath);
const tsConfigJsonPath = path.resolve(basePath, tsconfig);
return {
basePath,
runfilesPath,
angularCorePath,
write,
read,
writeFiles,
writeConfig,
shouldExist,
shouldNotExist,
runOneBuild: runOneBuildImpl
};
// -----------------
// helpers
function write(fileName: string, content: string) {
const dir = path.dirname(fileName);
if (dir != '.') {
const newDir = path.resolve(basePath, dir);
if (!fs.existsSync(newDir)) fs.mkdirSync(newDir);
}
fs.writeFileSync(path.resolve(basePath, fileName), content, {encoding: 'utf-8'});
}
function read(fileName: string) {
return fs.readFileSync(path.resolve(basePath, fileName), {encoding: 'utf-8'});
}
function writeFiles(...mockDirs: {[fileName: string]: string}[]) {
mockDirs.forEach(
(dir) => { Object.keys(dir).forEach((fileName) => { write(fileName, dir[fileName]); }); });
}
function writeConfig({
srcTargetPath, depPaths = [], pathMapping = [],
}: {
srcTargetPath: string,
depPaths?: string[],
pathMapping?: Array<{moduleName: string; path: string;}>,
}) {
srcTargetPath = path.resolve(basePath, srcTargetPath);
const compilationTargetSrc = listFilesRecursive(srcTargetPath);
const target = '//' + path.relative(basePath, srcTargetPath);
const files = [...compilationTargetSrc];
depPaths = depPaths.concat([angularCorePath]);
pathMapping = pathMapping.concat([{moduleName: '@angular/core', path: angularCorePath}]);
for (const depPath of depPaths) {
files.push(...listFilesRecursive(depPath).filter(f => f.endsWith('.d.ts')));
}
const pathMappingObj = {};
for (const mapping of pathMapping) {
pathMappingObj[mapping.moduleName] = [mapping.path];
pathMappingObj[path.join(mapping.moduleName, '*')] = [path.join(mapping.path, '*')];
}
const emptyTsConfig = ts.readConfigFile(
path.resolve(
runfilesPath, 'angular', 'test', 'ngc-wrapped', 'empty', 'empty_tsconfig.json'),
read);
const tsconfig = createTsConfig({
defaultTsConfig: emptyTsConfig.config,
rootDir: basePath,
target: target,
outDir: bazelBinPath, compilationTargetSrc,
files: files,
pathMapping: pathMappingObj,
});
write(path.resolve(basePath, tsConfigJsonPath), JSON.stringify(tsconfig, null, 2));
}
function shouldExist(fileName: string) {
if (!fs.existsSync(path.resolve(basePath, fileName))) {
throw new Error(`Expected ${fileName} to be emitted (basePath: ${basePath})`);
}
}
function shouldNotExist(fileName: string) {
if (fs.existsSync(path.resolve(basePath, fileName))) {
throw new Error(`Did not expect ${fileName} to be emitted (basePath: ${basePath})`);
}
}
function runOneBuildImpl(): boolean { return runOneBuild(['@' + tsConfigJsonPath]); }
}
function makeTempDir(baseDir): string {
const id = (Math.random() * 1000000).toFixed(0);
const dir = path.join(baseDir, `tmp.${id}`);
fs.mkdirSync(dir);
return dir;
}
export function listFilesRecursive(dir: string, fileList: string[] = []) {
fs.readdirSync(dir).map(file => {
if (fs.statSync(path.join(dir, file)).isDirectory()) {
listFilesRecursive(path.join(dir, file), fileList);
} else {
fileList.push(path.join(dir, file));
}
});
return fileList;
}

View File

@ -0,0 +1,6 @@
{
"compilerOptions": {
"lib": ["es5", "es2015.collection", "es2015.core"],
"types": ["node", "jasmine"]
}
}

View File

@ -0,0 +1,98 @@
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import * as path from 'path';
import * as ts from 'typescript';
const EXT = /(\.ts|\.d\.ts|\.js|\.jsx|\.tsx)$/;
export interface TsConfigOptions {
defaultTsConfig: any;
outDir: string;
rootDir: string;
pathMapping: {[pattern: string]: string[]};
// e.g. //packages/core:core
target: string;
compilationTargetSrc: string[];
files: string[];
}
/**
* Creates a tsconfig based on the default tsconfig
* to adjust paths, ...
*
* @param options
*/
export function createTsConfig(options: TsConfigOptions) {
const result = options.defaultTsConfig;
return {
'extends': '../angular/test/ngc-wrapped/empty/tsconfig',
'compilerOptions': {
...result.compilerOptions,
'outDir': options.outDir,
'rootDir': options.rootDir,
'rootDirs': [
options.rootDir,
],
'baseUrl': options.rootDir,
'paths': {
'*': [
'./*',
],
...options.pathMapping,
},
// we have to set this as the default tsconfig is made of es6 mode
'target': 'es5',
// we have to set this as the default tsconfig is made of es6 mode
'module': 'commonjs',
// if we specify declarationDir, we also have to specify
// declaration in the same tsconfig.json, otherwise ts will error.
'declaration': true,
'declarationDir': options.outDir,
},
'bazelOptions': {
...result.bazelOptions,
'workspaceName': 'angular',
'target': options.target,
// we have to set this as the default tsconfig is made of es6 mode
'es5Mode': true,
'manifest': createManifestPath(options),
'compilationTargetSrc': options.compilationTargetSrc,
},
'files': options.files,
'angularCompilerOptions': {
...result.angularCompilerOptions,
'expectedOut': [
...options.compilationTargetSrc.map(src => srcToExpectedOut(src, 'js', options)),
...options.compilationTargetSrc.map(src => srcToExpectedOut(src, 'd.ts', options)),
...options.compilationTargetSrc.map(src => srcToExpectedOut(src, 'ngfactory.js', options)),
...options.compilationTargetSrc.map(
src => srcToExpectedOut(src, 'ngfactory.d.ts', options)),
...options.compilationTargetSrc.map(src => srcToExpectedOut(src, 'ngsummary.js', options)),
...options.compilationTargetSrc.map(
src => srcToExpectedOut(src, 'ngsummary.d.ts', options)),
...options.compilationTargetSrc.map(
src => srcToExpectedOut(src, 'ngsummary.json', options)),
]
}
};
}
function srcToExpectedOut(srcFile: string, suffix: string, options: TsConfigOptions): string {
const baseName = path.basename(srcFile).replace(EXT, '');
return path.join(
path.relative(options.rootDir, options.outDir),
path.relative(options.rootDir, path.dirname(srcFile)), baseName) +
'.' + suffix;
}
function createManifestPath(options: TsConfigOptions): string {
return path.resolve(options.outDir, options.target.replace(/\/\/|@/g, '').replace(/:/g, '/')) +
'.es5.MF';
}

View File

@ -8,7 +8,7 @@
"dependencies": {
"@angular/core": "^2.0.0-rc.7",
"reflect-metadata": "^0.1.2",
"rxjs": "^5.0.1",
"rxjs": "^5.5.0",
"jpm": "1.1.4",
"firefox-profile": "0.4.0",
"selenium-webdriver": "^2.53.3"

View File

@ -191,6 +191,14 @@ export class HttpXhrBackend implements HttpBackend {
// The parse error contains the text of the body that failed to parse.
body = { error, text: body } as HttpJsonParseError;
}
} else if (!ok && req.responseType === 'json' && typeof body === 'string') {
try {
// Attempt to parse the body as JSON.
body = JSON.parse(body);
} catch (error) {
// Cannot be certain that the body was meant to be parsed as JSON.
// Leave the body as a string.
}
}
if (ok) {

View File

@ -17,7 +17,7 @@ import {MockXhrFactory} from './xhr_mock';
function trackEvents(obs: Observable<HttpEvent<any>>): HttpEvent<any>[] {
const events: HttpEvent<any>[] = [];
obs.subscribe(event => events.push(event));
obs.subscribe(event => events.push(event), err => events.push(err));
return events;
}
@ -92,6 +92,13 @@ export function main() {
const res = events[1] as HttpResponse<{data: string}>;
expect(res.body !.data).toBe('some data');
});
it('handles a json error response', () => {
const events = trackEvents(backend.handle(TEST_POST.clone({responseType: 'json'})));
factory.mock.mockFlush(500, 'Error', JSON.stringify({data: 'some data'}));
expect(events.length).toBe(2);
const res = events[1] as any as HttpErrorResponse;
expect(res.error !.data).toBe('some data');
});
it('handles a json string response', () => {
const events = trackEvents(backend.handle(TEST_POST.clone({responseType: 'json'})));
expect(factory.mock.responseType).toEqual('text');

View File

@ -12,6 +12,7 @@
"tslib": "^1.7.1"
},
"peerDependencies": {
"rxjs": "^5.5.0",
"@angular/core": "0.0.0-PLACEHOLDER"
},
"repository": {

View File

@ -6,13 +6,10 @@
* found in the LICENSE file at https://angular.io/license
*/
export {AotCompilerHost, AotCompilerHost as StaticReflectorHost, StaticReflector, StaticSymbol} from '@angular/compiler';
export {CodeGenerator} from './src/codegen';
export {CompilerHost, CompilerHostContext, ModuleResolutionHostAdapter, NodeCompilerHostContext} from './src/compiler_host';
export {DiagnosticTemplateInfo, getExpressionScope, getTemplateExpressionDiagnostics} from './src/diagnostics/expression_diagnostics';
export {AstType, ExpressionDiagnosticsContext} from './src/diagnostics/expression_type';
export {BuiltinType, DeclarationKind, Definition, PipeInfo, Pipes, Signature, Span, Symbol, SymbolDeclaration, SymbolQuery, SymbolTable} from './src/diagnostics/symbols';
export {getClassMembersFromDeclaration, getPipesTable, getSymbolQuery} from './src/diagnostics/typescript_symbols';
export {Extractor} from './src/extractor';
export {VERSION} from './src/version';
export * from './src/metadata';
@ -21,8 +18,7 @@ export * from './src/transformers/entry_points';
export * from './src/perform_compile';
// TODO(tbosch): remove this once everyone is on transformers
// TODO(tbosch): remove this once cli 1.5 is fully released,
// and usages in G3 are changed to `CompilerOptions`.
export {CompilerOptions as AngularCompilerOptions} from './src/transformers/api';
// TODO(hansl): moving to Angular 4 need to update this API.
export {NgTools_InternalApi_NG_2 as __NGTOOLS_PRIVATE_API_2} from './src/ngtools_api';

View File

@ -34,10 +34,10 @@ const EXPECTED_XMB = `<?xml version="1.0" encoding="UTF-8" ?>
<!ELEMENT ex (#PCDATA)>
]>
<messagebundle>
<msg id="8136548302122759730" desc="desc" meaning="meaning"><source>src/basic.ts:1</source>translate me</msg>
<msg id="3492007542396725315"><source>src/basic.ts:5</source><source>src/entry_components.ts:1</source>Welcome</msg>
<msg id="126808141597411718"><source>node_modules/third_party/other_comp.d.ts:1,2</source>other-3rdP-component
multi-lines</msg>
<msg id="8136548302122759730" desc="desc" meaning="meaning"><source>src/basic.ts:1</source>translate me</msg>
<msg id="3492007542396725315"><source>src/basic.ts:5</source><source>src/entry_components.ts:1</source>Welcome</msg>
</messagebundle>
`;
@ -45,6 +45,14 @@ const EXPECTED_XLIFF = `<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2">
<file source-language="fr" datatype="plaintext" original="ng2.template">
<body>
<trans-unit id="b0a17f08a4bd742b2acf39780c257c2f519d33ed" datatype="html">
<source>other-3rdP-component
multi-lines</source>
<context-group purpose="location">
<context context-type="sourcefile">node_modules/third_party/other_comp.d.ts</context>
<context context-type="linenumber">1</context>
</context-group>
</trans-unit>
<trans-unit id="76e1eccb1b772fa9f294ef9c146ea6d0efa8a2d4" datatype="html">
<source>translate me</source>
<context-group purpose="location">
@ -65,14 +73,6 @@ const EXPECTED_XLIFF = `<?xml version="1.0" encoding="UTF-8" ?>
<context context-type="linenumber">1</context>
</context-group>
</trans-unit>
<trans-unit id="b0a17f08a4bd742b2acf39780c257c2f519d33ed" datatype="html">
<source>other-3rdP-component
multi-lines</source>
<context-group purpose="location">
<context context-type="sourcefile">node_modules/third_party/other_comp.d.ts</context>
<context context-type="linenumber">1</context>
</context-group>
</trans-unit>
</body>
</file>
</xliff>
@ -81,6 +81,15 @@ multi-lines</source>
const EXPECTED_XLIFF2 = `<?xml version="1.0" encoding="UTF-8" ?>
<xliff version="2.0" xmlns="urn:oasis:names:tc:xliff:document:2.0" srcLang="en">
<file original="ng.template" id="ngi18n">
<unit id="126808141597411718">
<notes>
<note category="location">node_modules/third_party/other_comp.d.ts:1,2</note>
</notes>
<segment>
<source>other-3rdP-component
multi-lines</source>
</segment>
</unit>
<unit id="8136548302122759730">
<notes>
<note category="description">desc</note>
@ -100,15 +109,6 @@ const EXPECTED_XLIFF2 = `<?xml version="1.0" encoding="UTF-8" ?>
<source>Welcome</source>
</segment>
</unit>
<unit id="126808141597411718">
<notes>
<note category="location">node_modules/third_party/other_comp.d.ts:1,2</note>
</notes>
<segment>
<source>other-3rdP-component
multi-lines</source>
</segment>
</unit>
</file>
</xliff>
`;

View File

@ -15,8 +15,6 @@ import * as ts from 'typescript';
import * as assert from 'assert';
import {__NGTOOLS_PRIVATE_API_2, readConfiguration} from '@angular/compiler-cli';
const glob = require('glob');
/* tslint:disable:no-console */
/**
* Main method.
@ -27,9 +25,6 @@ function main() {
console.log(`testing ngtools API...`);
Promise.resolve()
.then(() => codeGenTest())
.then(() => codeGenTest(true))
.then(() => i18nTest())
.then(() => lazyRoutesTest())
.then(() => {
console.log('All done!');
@ -42,152 +37,6 @@ function main() {
});
}
function codeGenTest(forceError = false) {
const basePath = path.join(__dirname, '../ngtools_src');
const srcPath = path.join(__dirname, '../src');
const project = path.join(basePath, 'tsconfig-build.json');
const readResources: string[] = [];
const wroteFiles: string[] = [];
const config = readConfiguration(project);
const delegateHost = ts.createCompilerHost(config.options, true);
const host: ts.CompilerHost = Object.assign(
{}, delegateHost,
{writeFile: (fileName: string, ...rest: any[]) => { wroteFiles.push(fileName); }});
const program = ts.createProgram(config.rootNames, config.options, host);
config.options.basePath = basePath;
console.log(`>>> running codegen for ${project}`);
if (forceError) {
console.log(`>>> asserting that missingTranslation param with error value throws`);
}
return __NGTOOLS_PRIVATE_API_2
.codeGen({
basePath,
compilerOptions: config.options, program, host,
angularCompilerOptions: config.options,
// i18n options.
i18nFormat: 'xlf',
i18nFile: path.join(srcPath, 'messages.fi.xlf'),
locale: 'fi',
missingTranslation: forceError ? 'error' : 'ignore',
readResource: (fileName: string) => {
readResources.push(fileName);
if (!host.fileExists(fileName)) {
throw new Error(`Compilation failed. Resource file not found: ${fileName}`);
}
return Promise.resolve(host.readFile(fileName));
}
})
.then(() => {
console.log(`>>> codegen done, asserting read and wrote files`);
// Assert for each file that it has been read and each `ts` has a written file associated.
const allFiles = glob.sync(path.join(basePath, '**/*'), {nodir: true});
allFiles.forEach((fileName: string) => {
// Skip tsconfig.
if (fileName.match(/tsconfig-build.json$/)) {
return;
}
// Assert that file was read.
if (fileName.match(/\.module\.ts$/)) {
const factory = fileName.replace(/\.module\.ts$/, '.module.ngfactory.ts');
assert(wroteFiles.indexOf(factory) != -1, `Expected file "${factory}" to be written.`);
} else if (fileName.match(/\.css$/) || fileName.match(/\.html$/)) {
assert(
readResources.indexOf(fileName) != -1,
`Expected resource "${fileName}" to be read.`);
}
});
console.log(`done, no errors.`);
})
.catch((e: Error) => {
if (forceError) {
assert(
e.message.match(`Missing translation for message`),
`Expected error message for missing translations`);
console.log(`done, error catched`);
} else {
console.error(e.stack);
console.error('Compilation failed');
throw e;
}
});
}
function i18nTest() {
const basePath = path.join(__dirname, '../ngtools_src');
const project = path.join(basePath, 'tsconfig-build.json');
const readResources: string[] = [];
const wroteFiles: string[] = [];
const config = readConfiguration(project);
const delegateHost = ts.createCompilerHost(config.options, true);
const host: ts.CompilerHost = Object.assign(
{}, delegateHost,
{writeFile: (fileName: string, ...rest: any[]) => { wroteFiles.push(fileName); }});
const program = ts.createProgram(config.rootNames, config.options, host);
config.options.basePath = basePath;
console.log(`>>> running i18n extraction for ${project}`);
return __NGTOOLS_PRIVATE_API_2
.extractI18n({
basePath,
compilerOptions: config.options, program, host,
angularCompilerOptions: config.options,
i18nFormat: 'xlf',
locale: undefined,
outFile: undefined,
readResource: (fileName: string) => {
readResources.push(fileName);
if (!host.fileExists(fileName)) {
throw new Error(`Compilation failed. Resource file not found: ${fileName}`);
}
return Promise.resolve(host.readFile(fileName));
},
})
.then(() => {
console.log(`>>> i18n extraction done, asserting read and wrote files`);
const allFiles = glob.sync(path.join(basePath, '**/*'), {nodir: true});
assert(wroteFiles.length == 1, `Expected a single message bundle file.`);
assert(
wroteFiles[0].endsWith('/ngtools_src/messages.xlf'),
`Expected the bundle file to be "message.xlf".`);
allFiles.forEach((fileName: string) => {
// Skip tsconfig.
if (fileName.match(/tsconfig-build.json$/)) {
return;
}
// Assert that file was read.
if (fileName.match(/\.css$/) || fileName.match(/\.html$/)) {
assert(
readResources.indexOf(fileName) != -1,
`Expected resource "${fileName}" to be read.`);
}
});
console.log(`done, no errors.`);
})
.catch((e: Error) => {
console.error(e.stack);
console.error('Extraction failed');
throw e;
});
}
function lazyRoutesTest() {
const basePath = path.join(__dirname, '../ngtools_src');
const project = path.join(basePath, 'tsconfig-build.json');

View File

@ -1,126 +0,0 @@
#!/usr/bin/env node
/**
* @license
* Copyright Google Inc. All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
// Must be imported first, because Angular decorators throw on load.
import 'reflect-metadata';
import * as path from 'path';
import * as ts from 'typescript';
import * as assert from 'assert';
import {CompilerOptions, CodeGenerator, CompilerHostContext, NodeCompilerHostContext, readConfiguration} from '@angular/compiler-cli';
/* tslint:disable:no-console */
/**
* Main method.
* Standalone program that executes the real codegen and tests that
* ngsummary.json files are used for libraries.
*/
function main() {
console.log(`testing usage of ngsummary.json files in libraries...`);
const basePath = path.resolve(__dirname, '..');
const project = path.resolve(basePath, 'tsconfig-build.json');
const readFiles: string[] = [];
const writtenFiles: {fileName: string, content: string}[] = [];
class AssertingHostContext extends NodeCompilerHostContext {
readFile(fileName: string): string {
if (/.*\/node_modules\/.*/.test(fileName) && !/.*ngsummary\.json$/.test(fileName) &&
!/package\.json$/.test(fileName)) {
// Only allow to read summaries and package.json files from node_modules
// TODO (mhevery): Fix this. TypeScript.d.ts does not allow returning null.
return null !;
}
readFiles.push(path.relative(basePath, fileName));
return super.readFile(fileName);
}
readResource(s: string): Promise<string> {
readFiles.push(path.relative(basePath, s));
return super.readResource(s);
}
}
const config = readConfiguration(project);
config.options.basePath = basePath;
// This flag tells ngc do not recompile libraries.
config.options.generateCodeForLibraries = false;
console.log(`>>> running codegen for ${project}`);
codegen(
config,
(host) => {
host.writeFile = (fileName: string, content: string) => {
fileName = path.relative(basePath, fileName);
writtenFiles.push({fileName, content});
};
return new AssertingHostContext();
})
.then((exitCode: any) => {
console.log(`>>> codegen done, asserting read files`);
assertSomeFileMatch(readFiles, /^node_modules\/.*\.ngsummary\.json$/);
assertNoFileMatch(readFiles, /^node_modules\/.*\.metadata.json$/);
assertNoFileMatch(readFiles, /^node_modules\/.*\.html$/);
assertNoFileMatch(readFiles, /^node_modules\/.*\.css$/);
assertNoFileMatch(readFiles, /^src\/.*\.ngsummary\.json$/);
assertSomeFileMatch(readFiles, /^src\/.*\.html$/);
assertSomeFileMatch(readFiles, /^src\/.*\.css$/);
console.log(`>>> asserting written files`);
assertWrittenFile(writtenFiles, /^src\/module\.ngfactory\.ts$/, /class MainModuleInjector/);
console.log(`done, no errors.`);
process.exit(exitCode);
})
.catch((e: any) => {
console.error(e.stack);
console.error('Compilation failed');
process.exit(1);
});
}
/**
* Simple adaption of main to just run codegen with a CompilerHostContext
*/
function codegen(
config: {options: CompilerOptions, rootNames: string[]},
hostContextFactory: (host: ts.CompilerHost) => CompilerHostContext) {
const host = ts.createCompilerHost(config.options, true);
// HACK: patch the realpath to solve symlink issue here:
// https://github.com/Microsoft/TypeScript/issues/9552
// todo(misko): remove once facade symlinks are removed
host.realpath = (path) => path;
const program = ts.createProgram(config.rootNames, config.options, host);
return CodeGenerator.create(config.options, {
} as any, program, host, hostContextFactory(host)).codegen();
}
function assertSomeFileMatch(fileNames: string[], pattern: RegExp) {
assert(
fileNames.some(fileName => pattern.test(fileName)),
`Expected some read files match ${pattern}`);
}
function assertNoFileMatch(fileNames: string[], pattern: RegExp) {
const matches = fileNames.filter(fileName => pattern.test(fileName));
assert(
matches.length === 0,
`Expected no read files match ${pattern}, but found: \n${matches.join('\n')}`);
}
function assertWrittenFile(
files: {fileName: string, content: string}[], filePattern: RegExp, contentPattern: RegExp) {
assert(
files.some(file => filePattern.test(file.fileName) && contentPattern.test(file.content)),
`Expected some written files for ${filePattern} and content ${contentPattern}`);
}
main();

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